/* WirePlumber * * Copyright © 2020 Collabora Ltd. * @author Arnaud Ferraris * * SPDX-License-Identifier: MIT */ #include #include #include #include #include #include #define STATE_NAME "default-routes" #define SAVE_INTERVAL_MS 1000 G_DEFINE_QUARK (wp-module-default-routes-routes, routes); /* Signals */ enum { SIGNAL_GET_ROUTES, LAST_SIGNAL }; static guint signals[LAST_SIGNAL] = { 0 }; G_DECLARE_DERIVABLE_TYPE (WpDefaultRoutes, wp_default_routes, WP, DEFAULT_ROUTES, WpPlugin) struct _WpDefaultRoutesClass { WpPluginClass parent_class; void (*get_routes) (WpDefaultRoutes *self, WpPipewireObject *device, GHashTable **routes); }; typedef struct _WpDefaultRoutesPrivate WpDefaultRoutesPrivate; struct _WpDefaultRoutesPrivate { WpState *state; WpProperties *routes; WpProperties *props; GSource *routes_timeout; GSource *props_timeout; GHashTable *current_routes; GHashTable *default_routes; WpObjectManager *devices_om; }; G_DEFINE_TYPE_WITH_PRIVATE (WpDefaultRoutes, wp_default_routes, WP_TYPE_PLUGIN) #define ALLOW_CHARS "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-_." #define MAX_JSON_STRING_LEN 256 static gint find_device_route (WpPipewireObject *device, const gchar *lookup_name, gint lookup_device) { WpIterator *routes = NULL; g_auto (GValue) item = G_VALUE_INIT; routes = g_object_get_qdata (G_OBJECT (device), routes_quark ()); g_return_val_if_fail (routes, -1); wp_iterator_reset (routes); for (; wp_iterator_next (routes, &item); g_value_unset (&item)) { WpSpaPod *pod = g_value_get_boxed (&item); gint index = 0; const gchar *name = NULL; guint size, type, num, i; gint *devlist = NULL; /* Parse */ if (!wp_spa_pod_get_object (pod, NULL, "index", "i", &index, "name", "s", &name, "devices", "a", &size, &type, &num, &devlist, NULL)) { continue; } for (i = 0; i < num; i++) { if (devlist[i] == lookup_device && g_strcmp0 (name, lookup_name) == 0) return index; } } return -1; } static gboolean timeout_save_properties_cb (WpDefaultRoutes *self) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); if (!wp_state_save (priv->state, "properties", priv->props)) wp_warning_object (self, "could not save properties"); return G_SOURCE_REMOVE; } static void timeout_save_properties (WpDefaultRoutes *self, guint ms) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_return_if_fail (core); g_return_if_fail (priv->props); /* Clear the current timeout callback */ if (priv->props_timeout) g_source_destroy (priv->props_timeout); g_clear_pointer (&priv->props_timeout, g_source_unref); /* Add the timeout callback */ wp_core_timeout_add_closure (core, &priv->props_timeout, ms, g_cclosure_new_object (G_CALLBACK (timeout_save_properties_cb), G_OBJECT (self))); } static uint32_t channel_from_name (const char *name) { for (gint i = 0; spa_type_audio_channel[i].name; i++) { if (g_strcmp0 (name, spa_debug_type_short_name (spa_type_audio_channel[i].name)) == 0) return spa_type_audio_channel[i].type; } return SPA_AUDIO_CHANNEL_UNKNOWN; } static const char * channel_to_name(guint channel) { for (gint i = 0; spa_type_audio_channel[i].name; i++) { if (spa_type_audio_channel[i].type == channel) return spa_debug_type_short_name (spa_type_audio_channel[i].name); } return "UNK"; } static GArray * parse_channel_volumes(const gchar *chvol_str) { struct spa_json array; struct spa_json item; GArray *values = NULL; float val; spa_json_init (&array, chvol_str, strlen (chvol_str)); if (spa_json_enter_array (&array, &item) <= 0) return NULL; values = g_array_new (FALSE, FALSE, sizeof (float)); while (spa_json_get_float (&item, &val) > 0) g_array_append_val (values, val); return values; } static GPtrArray * parse_channel_map (const gchar *chmap_str) { struct spa_json array; struct spa_json item; GPtrArray *values; char val[MAX_JSON_STRING_LEN]; spa_json_init (&array, chmap_str, strlen (chmap_str)); if (spa_json_enter_array (&array, &item) <= 0) return NULL; values = g_ptr_array_new (); g_ptr_array_set_free_func (values, g_free); while (spa_json_get_string (&item, val, MAX_JSON_STRING_LEN) > 0) g_ptr_array_add (values, g_strdup (val)); return values; } static void apply_routes_properties (WpDefaultRoutes *self, WpPipewireObject *device, const gchar *name, WpDirection direction, gint device_id) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_autoptr (WpSpaPodBuilder) builder = wp_spa_pod_builder_new_object ("Spa:Pod:Object:Param:Props", "Route"); g_autoptr (WpSpaPod) props = NULL; const gchar *dev_name = NULL; const gchar *prop_value = NULL; gchar *prop_name = NULL; gchar *routes_name = NULL; gchar *dir_str = direction == WP_DIRECTION_INPUT ? "input" : "output"; gint index; g_return_if_fail (priv->props); index = find_device_route (device, name, device_id); if (index < 0) { wp_info_object (self, "route '%s' (%d) is not valid", name, index); return; } /* Get the device name */ dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME); g_return_if_fail (dev_name); /* * Property keys cannot contain spaces or brackets, * make sure we use a valid string */ routes_name = g_strcanon (g_strdup (name), ALLOW_CHARS, '_'); prop_name = g_strdup_printf ("%s:%s:%s:%s", dev_name, dir_str, routes_name, "volume"); prop_value = wp_properties_get (priv->props, prop_name); g_free (prop_name); if (prop_value) { wp_spa_pod_builder_add_property (builder, "volume"); wp_spa_pod_builder_add_float (builder, strtof (prop_value, NULL)); } prop_name = g_strdup_printf ("%s:%s:%s:%s", dev_name, dir_str, routes_name, "mute"); prop_value = wp_properties_get (priv->props, prop_name); g_free (prop_name); if (prop_value) { wp_spa_pod_builder_add_property (builder, "mute"); wp_spa_pod_builder_add_boolean (builder, g_strcmp0 (prop_value, "true") == 0 ? TRUE : FALSE); } prop_name = g_strdup_printf ("%s:%s:%s:%s", dev_name, dir_str, routes_name, "channelVolumes"); prop_value = wp_properties_get (priv->props, prop_name); g_free (prop_name); if (prop_value) { g_autoptr (GArray) values = parse_channel_volumes (prop_value); if (values) { g_autoptr (WpSpaPodBuilder) chvol_builder = wp_spa_pod_builder_new_array (); /* Build the channelVolumes array spa pod */ for (guint i = 0; i < values->len; i++) { wp_spa_pod_builder_add_float (chvol_builder, g_array_index (values, float, i)); } /* Add the channelVolumes property */ wp_spa_pod_builder_add_property (builder, "channelVolumes"); g_autoptr (WpSpaPod) chvol = wp_spa_pod_builder_end (chvol_builder); wp_spa_pod_builder_add_pod (builder, chvol); } } prop_name = g_strdup_printf ("%s:%s:%s:%s", dev_name, dir_str, routes_name, "channelMap"); prop_value = wp_properties_get (priv->props, prop_name); g_free (prop_name); if (prop_value) { g_autoptr (GPtrArray) values = parse_channel_map (prop_value); if (values) { g_autoptr (WpSpaPodBuilder) chmap_builder = wp_spa_pod_builder_new_array (); /* Build the channelMap array spa pod */ for (guint i = 0; i < values->len; i++) { gchar *channel = g_ptr_array_index (values, i); wp_spa_pod_builder_add_id (chmap_builder, channel_from_name (channel)); } /* Add the channelMap property */ wp_spa_pod_builder_add_property (builder, "channelMap"); g_autoptr (WpSpaPod) chmap = wp_spa_pod_builder_end (chmap_builder); wp_spa_pod_builder_add_pod (builder, chmap); } } props = wp_spa_pod_builder_end (builder); wp_pipewire_object_set_param (device, "Route", 0, wp_spa_pod_new_object ( "Spa:Pod:Object:Param:Route", "Route", "index", "i", index, "device", "i", device_id, "props", "O", props, NULL)); wp_info_object (self, "properties set for route %d on " WP_OBJECT_FORMAT, index, WP_OBJECT_ARGS (device)); } static void save_routes_properties (WpDefaultRoutes *self, WpPipewireObject *device, const gchar *name, WpDirection direction, WpSpaPod *properties) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); const gchar *dev_name = NULL; gchar *routes_name = NULL; gchar *prop_name = NULL; gchar *prop_value = NULL; g_autoptr (WpIterator) props = NULL; g_auto (GValue) item = G_VALUE_INIT; gchar *dir_str = direction == WP_DIRECTION_INPUT ? "input" : "output"; g_return_if_fail (properties); g_return_if_fail (priv->props); /* Get the device name */ dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME); g_return_if_fail (dev_name); routes_name = g_strcanon (g_strdup (name), ALLOW_CHARS, '_'); props = wp_spa_pod_new_iterator (properties); for (; wp_iterator_next (props, &item); g_value_unset (&item)) { WpSpaPod *pod = g_value_get_boxed (&item); const char *p_key = NULL; g_autoptr (WpSpaPod) p_val = NULL; wp_spa_pod_get_property (pod, &p_key, &p_val); prop_name = g_strdup_printf ("%s:%s:%s:%s", dev_name, dir_str, routes_name, p_key); /* volume */ if (g_strcmp0 (p_key, "volume") == 0) { float vol = 0.0f; wp_spa_pod_get_float (p_val, &vol); prop_value = g_strdup_printf ("%f", vol); } /* mute */ else if (g_strcmp0 (p_key, "mute") == 0) { gboolean b = FALSE; wp_spa_pod_get_boolean (p_val, &b); prop_value = g_strdup (b ? "true" : "false"); } /* channelVolumes */ else if (g_strcmp0 (p_key, "channelVolumes") == 0) { g_autoptr (WpIterator) it2 = wp_spa_pod_new_iterator (p_val); g_auto (GValue) item2 = G_VALUE_INIT; guint i = 0, n_vols = 0; float vols[SPA_AUDIO_MAX_CHANNELS]; for (; wp_iterator_next (it2, &item2); g_value_unset (&item2)) { float *vol = (float *) g_value_get_pointer (&item2); vols[n_vols] = *vol; n_vols++; } if (n_vols > 0) { size_t size; FILE *f; f = open_memstream (&prop_value, &size); fprintf (f, "[ "); for (i = 0; i < n_vols; i++) fprintf (f, "%s%f", i > 0 ? ", " : "", vols[i]); fprintf (f, " ]"); fclose (f); } } /* channelMap */ else if (g_strcmp0 (p_key, "channelMap") == 0) { g_autoptr (WpIterator) it2 = wp_spa_pod_new_iterator (p_val); g_auto (GValue) item2 = G_VALUE_INIT; guint i = 0, n_vals = 0; guint vals[SPA_AUDIO_MAX_CHANNELS]; for (; wp_iterator_next (it2, &item2); g_value_unset (&item2)) { guint *val = (guint *) g_value_get_pointer (&item2); vals[n_vals] = *val; n_vals++; } if (n_vals > 0) { size_t size; FILE *f; f = open_memstream (&prop_value, &size); fprintf (f, "[ "); for (i = 0; i < n_vals; i++) fprintf (f, "%s\"%s\"", i > 0 ? ", " : "", channel_to_name (vals[i])); fprintf (f, " ]"); fclose (f); } } if (prop_value) { wp_properties_set (priv->props, prop_name, prop_value); g_free (prop_value); } if (prop_name) g_free (prop_name); prop_name = prop_value = NULL; } g_free (routes_name); timeout_save_properties (self, SAVE_INTERVAL_MS); } static gchar * serialize_routes (GHashTable *routes) { GHashTableIter iter; const gchar *key; gpointer value; gchar *ptr; FILE *f; size_t size; gboolean first = TRUE; g_return_val_if_fail (routes, NULL); g_return_val_if_fail (g_hash_table_size (routes), NULL); f = open_memstream (&ptr, &size); g_return_val_if_fail (f, NULL); /* Routes are stored in a JSON array */ fprintf (f, "[ "); g_hash_table_iter_init (&iter, routes); while (g_hash_table_iter_next (&iter, (gpointer *) &key, &value)) { if (!first) fprintf (f, ", "); /* Each route is stored as a JSON object with "name" and "device" attributes */ fprintf (f, "{ \"name\": \"%s\", \"device\": %d }", key, GPOINTER_TO_INT (value)); first = FALSE; } fprintf (f, " ]"); fclose (f); return ptr; } static GHashTable * parse_routes (const gchar *routes_str) { struct spa_json array; struct spa_json item; struct spa_json object; GHashTable *routes; spa_json_init (&array, routes_str, strlen (routes_str)); if (spa_json_enter_array (&array, &item) <= 0) return NULL; routes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); while (spa_json_enter_object (&item, &object) > 0) { const char *prop; char name[MAX_JSON_STRING_LEN]; int dev_id, res; dev_id = -1; name[0] = 0; while ((res = spa_json_next (&object, &prop)) > 0) { if (strncmp (prop, "\"name\"", res) == 0) res = spa_json_get_string (&object, name, MAX_JSON_STRING_LEN); else if (strncmp (prop, "\"device\"", res) == 0) res = spa_json_get_int (&object, &dev_id); if (res <= 0) { g_critical ("unable to parse route"); g_hash_table_destroy (routes); return NULL; } } if (dev_id >= 0 && strlen(name) > 0) g_hash_table_insert (routes, g_strdup(name), GINT_TO_POINTER (dev_id)); } return routes; } static void hash_table_copy_elements (gpointer key, gpointer value, gpointer data) { GHashTable *ht = data; gchar *route = key; g_hash_table_insert (ht, g_strdup(route), value); } static gboolean timeout_save_routes_cb (WpDefaultRoutes *self) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); WpPipewireObject *device; GHashTableIter iter; GHashTable *ht; /* * `default_routes` is the reference list, used when other modules require * the default routes for a given device. * `current_routes` is a working copy, which is copied into `default_routes` * when saving to disk. */ g_hash_table_remove_all (priv->default_routes); g_hash_table_iter_init (&iter, priv->current_routes); while (g_hash_table_iter_next (&iter, (gpointer*) &device, (gpointer*) &ht)) { const gchar *dev_name; gchar *routes; GHashTable *copy = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME); if (!dev_name) continue; routes = serialize_routes (ht); if (routes) { wp_properties_set (priv->routes, dev_name, routes); g_free (routes); } g_hash_table_foreach (ht, hash_table_copy_elements, copy); g_hash_table_insert (priv->default_routes, device, copy); } if (!wp_state_save (priv->state, "routes", priv->routes)) wp_warning_object (self, "could not save routes"); return G_SOURCE_REMOVE; } static void timeout_save_routes (WpDefaultRoutes *self, guint ms) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); g_return_if_fail (core); g_return_if_fail (priv->routes); /* Clear the current timeout callback */ if (priv->routes_timeout) g_source_destroy (priv->routes_timeout); g_clear_pointer (&priv->routes_timeout, g_source_unref); /* Add the timeout callback */ wp_core_timeout_add_closure (core, &priv->routes_timeout, ms, g_cclosure_new_object (G_CALLBACK (timeout_save_routes_cb), G_OBJECT (self))); } static void wp_default_routes_get_routes (WpDefaultRoutes *self, WpPipewireObject *device, GHashTable **routes) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_return_if_fail (device); g_return_if_fail (routes); g_return_if_fail (priv->default_routes); *routes = g_hash_table_lookup (priv->default_routes, device); } static void update_routes (WpDefaultRoutes *self, WpPipewireObject *device, GHashTable *new_routes) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); GHashTable *curr_routes; GHashTableIter iter; g_return_if_fail (new_routes); g_return_if_fail (priv->routes); curr_routes = g_hash_table_lookup (priv->current_routes, device); /* Check if the new routes are the same as the current ones */ if (curr_routes) { if (g_hash_table_size (curr_routes) == g_hash_table_size (new_routes)) { gboolean identical = TRUE; const gchar *key; gpointer value; gint index; g_hash_table_iter_init (&iter, new_routes); while (g_hash_table_iter_next (&iter, (gpointer*) &key, &value)) { /* Make sure the route is valid */ index = find_device_route (device, key, GPOINTER_TO_INT (value)); if (index < 0) { wp_info_object (self, "route '%s' (%d) is not valid", key, index); return; } if (!g_hash_table_contains (curr_routes, key)) { identical = FALSE; break; } } if (identical) return; } } /* Otherwise update the route and add timeout save callback */ g_hash_table_insert (priv->current_routes, device, new_routes); timeout_save_routes (self, SAVE_INTERVAL_MS); } static void on_device_routes_notified (WpPipewireObject *device, GAsyncResult *res, WpDefaultRoutes *self) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_autoptr (WpIterator) routes = NULL; g_autoptr (GError) error = NULL; g_auto (GValue) item = G_VALUE_INIT; const gchar *name = NULL; gint device_id, direction = 0; GHashTable *new_routes; GHashTable *ht; /* Finish */ routes = wp_pipewire_object_enum_params_finish (device, res, &error); if (error) { wp_warning_object (self, "failed to get current route on device: %s", error->message); return; } new_routes = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, NULL); if (!new_routes) { wp_warning_object (self, "failed to allocate new hash table"); return; } ht = g_hash_table_lookup (priv->current_routes, device); for (; wp_iterator_next (routes, &item); g_value_unset (&item)) { /* Parse the route */ WpSpaPod *pod = g_value_get_boxed (&item); WpSpaPod *props; if (!wp_spa_pod_get_object (pod, NULL, "direction", "I", &direction, "device", "i", &device_id, "name", "s", &name, "props", "P", &props, NULL)) { wp_warning_object (self, "failed to parse current route"); continue; } if (ht) { if (g_hash_table_contains (ht, name)) save_routes_properties (self, device, name, direction, props); else if (g_hash_table_size (ht) > 0) /* * Apply route properties only once we have filled the `current_routes` * table for this device. This prevents race conditions when a device * appears. */ apply_routes_properties (self, device, name, direction, device_id); } g_hash_table_insert (new_routes, g_strdup (name), GINT_TO_POINTER (device_id)); } /* Update the routes list */ update_routes (self, device, new_routes); } static void on_device_param_info_notified (WpPipewireObject * device, GParamSpec * param, WpDefaultRoutes *self) { /* Check the route every time the params have changed */ wp_pipewire_object_enum_params (device, "Route", NULL, NULL, (GAsyncReadyCallback) on_device_routes_notified, self); } static void on_device_added (WpObjectManager *om, WpPipewireObject *device, gpointer d) { WpDefaultRoutes *self = WP_DEFAULT_ROUTES (d); WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_autoptr (WpIterator) it_routes = NULL; const gchar *dev_name = NULL; const gchar *routes = NULL; GHashTable *ht; wp_debug_object (self, "device " WP_OBJECT_FORMAT " added", WP_OBJECT_ARGS (device)); /* Enum available routes */ it_routes = wp_pipewire_object_enum_params_sync (device, "EnumRoute", NULL); if (!it_routes) return; /* Keep a reference of the routes in the device object */ g_object_set_qdata_full (G_OBJECT (device), routes_quark (), g_steal_pointer (&it_routes), (GDestroyNotify) wp_iterator_unref); /* Watch for param info changes */ g_signal_connect_object (device, "notify::param-info", G_CALLBACK (on_device_param_info_notified), self, 0); /* Load default routes for device */ dev_name = wp_pipewire_object_get_property (device, PW_KEY_DEVICE_NAME); g_return_if_fail (dev_name); routes = wp_properties_get (priv->routes, dev_name); if (!routes) return; ht = parse_routes (routes); g_return_if_fail (ht); g_hash_table_insert (priv->default_routes, device, ht); } static void on_device_removed (WpObjectManager *om, WpPipewireObject *device, gpointer d) { WpDefaultRoutes *self = WP_DEFAULT_ROUTES (d); WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); wp_debug_object (self, "device " WP_OBJECT_FORMAT " removed", WP_OBJECT_ARGS (device)); g_hash_table_remove (priv->current_routes, device); g_hash_table_remove (priv->default_routes, device); } static void wp_default_routes_enable (WpPlugin * plugin, WpTransition * transition) { g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (plugin)); WpDefaultRoutes *self = WP_DEFAULT_ROUTES (plugin); WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); /* Create the devices object manager */ priv->devices_om = wp_object_manager_new (); wp_object_manager_add_interest (priv->devices_om, WP_TYPE_DEVICE, NULL); wp_object_manager_request_object_features (priv->devices_om, WP_TYPE_DEVICE, WP_PIPEWIRE_OBJECT_FEATURES_ALL); g_signal_connect_object (priv->devices_om, "object-added", G_CALLBACK (on_device_added), self, 0); g_signal_connect_object (priv->devices_om, "object-removed", G_CALLBACK (on_device_removed), self, 0); wp_core_install_object_manager (core, priv->devices_om); wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); } static void wp_default_routes_disable (WpPlugin * plugin) { WpDefaultRoutes *self = WP_DEFAULT_ROUTES (plugin); WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); g_clear_object (&priv->devices_om); } static void wp_default_routes_finalize (GObject * object) { WpDefaultRoutes *self = WP_DEFAULT_ROUTES (object); WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); /* Clear the current timeout callback */ if (priv->props_timeout) g_source_destroy (priv->props_timeout); g_clear_pointer (&priv->props_timeout, g_source_unref); if (priv->routes_timeout) g_source_destroy (priv->routes_timeout); g_clear_pointer (&priv->routes_timeout, g_source_unref); g_hash_table_destroy (priv->current_routes); g_hash_table_destroy (priv->default_routes); g_clear_pointer (&priv->routes, wp_properties_unref); g_clear_object (&priv->state); } static void wp_default_routes_init (WpDefaultRoutes * self) { WpDefaultRoutesPrivate *priv = wp_default_routes_get_instance_private (self); priv->state = wp_state_new (STATE_NAME); wp_debug_object (self, "module default route loaded"); priv->current_routes = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_hash_table_destroy); priv->default_routes = g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL, (GDestroyNotify) g_hash_table_destroy); /* Load the saved routes */ priv->routes = wp_state_load (priv->state, "routes"); if (!priv->routes) { wp_warning_object (self, "could not load routes"); } /* Load the saved properties */ priv->props = wp_state_load (priv->state, "properties"); if (!priv->props) { wp_warning_object (self, "could not load properties"); } } static void wp_default_routes_class_init (WpDefaultRoutesClass * klass) { GObjectClass *object_class = (GObjectClass *) klass; WpPluginClass *plugin_class = (WpPluginClass *) klass; object_class->finalize = wp_default_routes_finalize; plugin_class->enable = wp_default_routes_enable; plugin_class->disable = wp_default_routes_disable; klass->get_routes = wp_default_routes_get_routes; /* Signals */ signals[SIGNAL_GET_ROUTES] = g_signal_new ("get-routes", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, G_STRUCT_OFFSET (WpDefaultRoutesClass, get_routes), NULL, NULL, NULL, G_TYPE_NONE, 2, WP_TYPE_DEVICE, G_TYPE_POINTER); } WP_PLUGIN_EXPORT gboolean wireplumber__module_init (WpCore * core, GVariant * args, GError ** error) { wp_plugin_register (g_object_new (wp_default_routes_get_type (), "name", STATE_NAME, "core", core, NULL)); return TRUE; }