summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorkotontrion <[email protected]>2024-07-16 15:13:00 +0200
committerkotontrion <[email protected]>2024-07-16 15:13:00 +0200
commitbccb03463fc41f2e6c4305a5565d4158a0f797fe (patch)
tree8bba78c11f79ea8ff44d3b16467768ebc010b6e3 /src
initial commit
Diffstat (limited to 'src')
-rw-r--r--src/endpoint.c206
-rw-r--r--src/meson.build69
-rw-r--r--src/wireplumber.c252
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);
+}