diff options
author | kotontrion <[email protected]> | 2024-07-16 15:13:00 +0200 |
---|---|---|
committer | kotontrion <[email protected]> | 2024-07-16 15:13:00 +0200 |
commit | bccb03463fc41f2e6c4305a5565d4158a0f797fe (patch) | |
tree | 8bba78c11f79ea8ff44d3b16467768ebc010b6e3 /src |
initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/endpoint.c | 206 | ||||
-rw-r--r-- | src/meson.build | 69 | ||||
-rw-r--r-- | src/wireplumber.c | 252 |
3 files changed, 527 insertions, 0 deletions
diff --git a/src/endpoint.c b/src/endpoint.c new file mode 100644 index 0000000..6dadda8 --- /dev/null +++ b/src/endpoint.c @@ -0,0 +1,206 @@ +#include <limits.h> +#include <wp/wp.h> + +#include "endpoint-private.h" +#include "glib-object.h" +#include "glib.h" + +struct _AstalWpEndpoint { + GObject parent_instance; + + guint id; + gdouble volume; + gboolean mute; + const gchar *description; +}; + +typedef struct { + WpNode *node; + WpPlugin *mixer; + +} AstalWpEndpointPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpEndpoint, astal_wp_endpoint, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_ENDPOINT_PROP_ID = 1, + ASTAL_WP_ENDPOINT_PROP_VOLUME, + ASTAL_WP_ENDPOINT_PROP_MUTE, + ASTAL_WP_ENDPOINT_PROP_DESCRIPTION, + ASTAL_WP_ENDPOINT_N_PROPERTIES, +} AstalWpEndpointProperties; + +typedef enum { + ASTAL_WP_ENDPOINT_SIGNAL_CHANGED, + ASTAL_WP_ENDPOINT_N_SIGNALS +} AstalWpEndpointSignals; + +static guint astal_wp_endpoint_signals[ASTAL_WP_ENDPOINT_N_SIGNALS] = { + 0, +}; +static GParamSpec *astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_N_PROPERTIES] = { + NULL, +}; + +void astal_wp_endpoint_update_volume(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + gdouble volume; + gboolean mute; + GVariant *variant = NULL; + + g_signal_emit_by_name(priv->mixer, "get-volume", self->id, &variant); + + if (variant == NULL) return; + + g_variant_lookup(variant, "volume", "d", &volume); + g_variant_lookup(variant, "mute", "b", &mute); + + if (mute != self->mute) { + self->mute = mute; + g_object_notify(G_OBJECT(self), "mute"); + } + + if (volume != self->volume) { + self->volume = volume; + g_object_notify(G_OBJECT(self), "volume"); + } + + g_signal_emit_by_name(self, "changed"); +} + +void astal_wp_endpoint_set_volume(AstalWpEndpoint *self, gdouble volume) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + GVariant *variant = NULL; + GVariantBuilder b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&b, "{sv}", "volume", g_variant_new_double(volume)); + variant = g_variant_builder_end(&b); + + g_signal_emit_by_name(priv->mixer, "set-volume", self->id, variant); + g_variant_unref(variant); +} + +void astal_wp_endpoint_set_mute(AstalWpEndpoint *self, gboolean mute) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + GVariant *variant = NULL; + GVariantBuilder b = G_VARIANT_BUILDER_INIT(G_VARIANT_TYPE_VARDICT); + g_variant_builder_add(&b, "{sv}", "mute", g_variant_new_boolean(mute)); + variant = g_variant_builder_end(&b); + + g_signal_emit_by_name(priv->mixer, "set-volume", self->id, variant); + + g_variant_unref(variant); +} + +static void astal_wp_endpoint_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + + switch (property_id) { + case ASTAL_WP_ENDPOINT_PROP_ID: + g_value_set_uint(value, self->id); + break; + case ASTAL_WP_ENDPOINT_PROP_MUTE: + g_value_set_boolean(value, self->mute); + break; + case ASTAL_WP_ENDPOINT_PROP_VOLUME: + g_value_set_double(value, self->volume); + break; + case ASTAL_WP_ENDPOINT_PROP_DESCRIPTION: + g_value_set_string(value, self->description); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_endpoint_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + + switch (property_id) { + case ASTAL_WP_ENDPOINT_PROP_MUTE: + astal_wp_endpoint_set_mute(self, g_value_get_boolean(value)); + break; + case ASTAL_WP_ENDPOINT_PROP_VOLUME: + astal_wp_endpoint_set_volume(self, g_value_get_double(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +AstalWpEndpoint *astal_wp_endpoint_create(WpNode *node, WpPlugin *mixer) { + AstalWpEndpoint *self = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + + priv->mixer = g_object_ref(mixer); + priv->node = g_object_ref(node); + + self->id = wp_proxy_get_bound_id(WP_PROXY(node)); + + astal_wp_endpoint_update_volume(self); + + const gchar *description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.description"); + if (description == NULL) { + description = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.nick"); + } + if (description == NULL) { + description = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(node), "node.name"); + } + if (description == NULL) { + description = "unknown"; + } + + self->description = g_strdup(description); + + return self; +} + +static void astal_wp_endpoint_init(AstalWpEndpoint *self) { + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + priv->node = NULL; + priv->mixer = NULL; + + self->volume = 0; + self->mute = TRUE; + self->description = NULL; +} + +static void astal_wp_endpoint_dispose(GObject *object) { + AstalWpEndpoint *self = ASTAL_WP_ENDPOINT(object); + AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); + g_clear_object(&priv->node); + g_clear_object(&priv->mixer); +} + +static void astal_wp_endpoint_finalize(GObject *object) {} + +static void astal_wp_endpoint_class_init(AstalWpEndpointClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->dispose = astal_wp_endpoint_dispose; + object_class->finalize = astal_wp_endpoint_finalize; + object_class->get_property = astal_wp_endpoint_get_property; + object_class->set_property = astal_wp_endpoint_set_property; + + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_ID] = + g_param_spec_uint("id", "id", "id", 0, UINT_MAX, 0, G_PARAM_READABLE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_VOLUME] = + g_param_spec_double("volume", "volume", "volume", 0, 1, 0, G_PARAM_READWRITE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_MUTE] = + g_param_spec_boolean("mute", "mute", "mute", TRUE, G_PARAM_READWRITE); + astal_wp_endpoint_properties[ASTAL_WP_ENDPOINT_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_WP_ENDPOINT_N_PROPERTIES, + astal_wp_endpoint_properties); + + astal_wp_endpoint_signals[ASTAL_WP_ENDPOINT_SIGNAL_CHANGED] = + g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} diff --git a/src/meson.build b/src/meson.build new file mode 100644 index 0000000..bdd5512 --- /dev/null +++ b/src/meson.build @@ -0,0 +1,69 @@ +srcs = files( + 'wireplumber.c', + 'endpoint.c', +) + +deps = [ + dependency('gobject-2.0'), + dependency('gio-2.0'), + dependency('wireplumber-0.5'), + # dependency('json-glib-1.0'), +] + +astal_wireplumber_lib = library( + 'astal-wireplumber', + sources : srcs, + include_directories : astal_wireplumber_inc, + dependencies : deps, + version : meson.project_version(), + install : true +) + +libastal_wireplumber = declare_dependency( + link_with : astal_wireplumber_lib, + include_directories : astal_wireplumber_inc) + +# astal_wireplumber_executable = executable( +# 'astal-wireplumber', +# files('astal-wireplumber.c'), +# dependencies : [ +# dependency('gobject-2.0'), +# dependency('gio-2.0'), +# dependency('json-glib-1.0'), +# libastal_wireplumber +# ], +# install : true) + +pkg_config_name = 'astal-wireplumber-' + lib_so_version + +if get_option('introspection') + gir = gnome.generate_gir( + astal_wireplumber_lib, + sources : srcs + astal_wireplumber_headers + astal_wireplumber_subheaders, + nsversion : '0.1', + namespace : 'AstalWp', + symbol_prefix : 'astal_wp', + identifier_prefix : 'AstalWp', + includes : ['GObject-2.0', 'Gio-2.0'], + header : 'astal/wireplumber.h', + export_packages : pkg_config_name, + install : true + ) + + if get_option('vapi') + gnome.generate_vapi( + pkg_config_name, + sources : [gir[0]], + packages : ['gobject-2.0', 'gio-2.0'], + install : true) + endif +endif + +pkg_config.generate( + name : 'astal-wireplumber', + version : meson.project_version(), + libraries : [astal_wireplumber_lib], + filebase : pkg_config_name, + subdirs : 'astal', + description : 'astal wireplumber module', + url : 'https://github.com/astal-sh/wireplumber') diff --git a/src/wireplumber.c b/src/wireplumber.c new file mode 100644 index 0000000..8c52bf2 --- /dev/null +++ b/src/wireplumber.c @@ -0,0 +1,252 @@ +#include "wireplumber.h" + +#include <wp/wp.h> + +#include "endpoint-private.h" +#include "glib-object.h" +#include "wp.h" + +struct _AstalWpWp { + GObject parent_instance; +}; + +typedef struct { + WpCore *core; + WpObjectManager *obj_manager; + + WpPlugin *mixer; + WpPlugin *defaults; + gint pending_plugins; + + GHashTable *endpoints; +} AstalWpWpPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpWp, astal_wp_wp, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_WP_SIGNAL_CHANGED, + ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED, + ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED, + ASTAL_WP_WP_N_SIGNALS +} AstalWpWpSignals; + +static guint astal_wp_wp_signals[ASTAL_WP_WP_N_SIGNALS] = { + 0, +}; + +/** + * astal_wp_wp_get_endpoint: + * @self: the AstalWpWp object + * @id: the id of the endpoint + * + * Returns: (transfer none) (nullable): the endpoint with the given id + */ +AstalWpEndpoint *astal_wp_wp_get_endpoint(AstalWpWp *self, guint id) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + AstalWpEndpoint *endpoint = g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id)); + return endpoint; +} + +static void astal_wp_wp_default_changed(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + guint defaultSinkId; + guint defaultSourceId; + + g_signal_emit_by_name(priv->defaults, "get-default-node", "Audio/Sink", &defaultSinkId); + g_signal_emit_by_name(priv->defaults, "get-default-node", "Audio/Source", &defaultSourceId); + + g_print("default nodes: sink: %d, source: %d\n", defaultSinkId, defaultSourceId); + + g_signal_emit_by_name(self, "changed"); +} + +static void astal_wp_wp_object_added(AstalWpWp *self, gpointer object) { + // print pipewire properties + // WpIterator *iter = wp_pipewire_object_new_properties_iterator(WP_PIPEWIRE_OBJECT(object)); + // GValue item = G_VALUE_INIT; + // const gchar *key, *value; + // + // g_print("\n\n"); + // while (wp_iterator_next (iter, &item)) { + // WpPropertiesItem *pi = g_value_get_boxed (&item); + // key = wp_properties_item_get_key (pi); + // value = wp_properties_item_get_value (pi); + // g_print("%s: %s\n", key, value); + // } + + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + WpNode *node = WP_NODE(object); + AstalWpEndpoint *endpoint = astal_wp_endpoint_create(node, priv->mixer); + + g_hash_table_insert(priv->endpoints, GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))), + endpoint); + + g_signal_emit_by_name(self, "endpoint-added", endpoint); + g_signal_emit_by_name(self, "changed"); +} + +static void astal_wp_wp_object_removed(AstalWpWp *self, gpointer object) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + WpNode *node = WP_NODE(object); + + guint id = wp_proxy_get_bound_id(WP_PROXY(node)); + + AstalWpEndpoint *endpoint = g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(id)); + + g_hash_table_remove(priv->endpoints, GUINT_TO_POINTER(id)); + + g_signal_emit_by_name(self, "endpoint-removed", endpoint); + g_signal_emit_by_name(self, "changed"); +} + +static void astal_wp_wp_mixer_changed(AstalWpWp *self, guint node_id) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + AstalWpEndpoint *endpoint = g_hash_table_lookup(priv->endpoints, GUINT_TO_POINTER(node_id)); + + if (endpoint == NULL) return; + + astal_wp_endpoint_update_volume(endpoint); + + g_signal_emit_by_name(self, "changed"); +} + +static void astal_wp_wp_objm_installed(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); +} + +static void astal_wp_wp_plugin_activated(WpObject *obj, GAsyncResult *result, AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + GError *error = NULL; + wp_object_activate_finish(obj, result, &error); + if (error) { + g_critical("Failed to activate component: %s\n", error->message); + return; + } + + if (--priv->pending_plugins == 0) { + priv->defaults = wp_plugin_find(priv->core, "default-nodes-api"); + priv->mixer = wp_plugin_find(priv->core, "mixer-api"); + + g_signal_connect_swapped(priv->mixer, "changed", (GCallback)astal_wp_wp_mixer_changed, + self); + g_signal_connect_swapped(priv->defaults, "changed", (GCallback)astal_wp_wp_default_changed, + self); + + g_signal_connect_swapped(priv->obj_manager, "object-added", + G_CALLBACK(astal_wp_wp_object_added), self); + g_signal_connect_swapped(priv->obj_manager, "object-removed", + G_CALLBACK(astal_wp_wp_object_removed), self); + wp_core_install_object_manager(priv->core, priv->obj_manager); + } +} + +static void astal_wp_wp_plugin_loaded(WpObject *obj, GAsyncResult *result, AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + GError *error = NULL; + wp_core_load_component_finish(priv->core, result, &error); + if (error) { + g_critical("Failed to load component: %s\n", error->message); + return; + } + + wp_object_activate(obj, WP_PLUGIN_FEATURE_ENABLED, NULL, + (GAsyncReadyCallback)astal_wp_wp_plugin_activated, self); +} + +/** + * astal_wp_wp_get_default + * + * Returns: (nullable) (transfer none): gets the default wireplumber object. + */ +AstalWpWp *astal_wp_wp_get_default() { + static AstalWpWp *self = NULL; + + if (self == NULL) self = g_object_new(ASTAL_WP_TYPE_WP, NULL); + + return self; +} + +/** + * astal_wp_get_default_wp + * + * Returns: (nullable) (transfer none): gets the default wireplumber object. + */ +AstalWpWp *astal_wp_get_default_wp() { return astal_wp_wp_get_default(); } + +static void astal_wp_wp_dispose(GObject *object) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + wp_core_disconnect(priv->core); + g_clear_object(&priv->mixer); + g_clear_object(&priv->defaults); + g_clear_object(&priv->obj_manager); + g_clear_object(&priv->core); + + if (priv->endpoints != NULL) { + g_hash_table_destroy(priv->endpoints); + priv->endpoints = NULL; + } +} + +static void astal_wp_wp_finalize(GObject *object) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); +} + +static void astal_wp_wp_init(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + priv->endpoints = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + + wp_init(WP_INIT_ALL); + priv->core = wp_core_new(NULL, NULL, NULL); + + if (!wp_core_connect(priv->core)) { + g_critical("could not connect to PipeWire\n"); + } + + priv->obj_manager = wp_object_manager_new(); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Audio/Sink", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Audio/Source", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", + "Stream/Output/Audio", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Input/Audio", NULL); + + g_signal_connect_swapped(priv->obj_manager, "installed", (GCallback)astal_wp_wp_objm_installed, + self); + + priv->pending_plugins = 2; + wp_core_load_component(priv->core, "libwireplumber-module-default-nodes-api", "module", NULL, + "default-nodes-api", NULL, + (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self); + wp_core_load_component(priv->core, "libwireplumber-module-mixer-api", "module", NULL, + "mixer-api", NULL, (GAsyncReadyCallback)astal_wp_wp_plugin_loaded, self); +} + +static void astal_wp_wp_class_init(AstalWpWpClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->finalize = astal_wp_wp_finalize; + object_class->dispose = astal_wp_wp_dispose; + + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED] = + g_signal_new("endpoint-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED] = + g_signal_new("endpoint-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, + NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_CHANGED] = + g_signal_new("changed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, + G_TYPE_NONE, 0); +} |