diff options
Diffstat (limited to 'wireplumber/src/wireplumber.c')
-rw-r--r-- | wireplumber/src/wireplumber.c | 503 |
1 files changed, 503 insertions, 0 deletions
diff --git a/wireplumber/src/wireplumber.c b/wireplumber/src/wireplumber.c new file mode 100644 index 0000000..f1fa516 --- /dev/null +++ b/wireplumber/src/wireplumber.c @@ -0,0 +1,503 @@ +#include <wp/wp.h> + +#include "audio.h" +#include "device-private.h" +#include "endpoint-private.h" +#include "glib-object.h" +#include "glib.h" +#include "video.h" +#include "wp.h" + +struct _AstalWpWp { + GObject parent_instance; + + AstalWpEndpoint *default_speaker; + AstalWpEndpoint *default_microphone; + + AstalWpAudio *audio; + AstalWpVideo *video; + + AstalWpScale scale; +}; + +typedef struct { + WpCore *core; + WpObjectManager *obj_manager; + + WpPlugin *mixer; + WpPlugin *defaults; + gint pending_plugins; + + GHashTable *endpoints; + GHashTable *devices; +} AstalWpWpPrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpWp, astal_wp_wp, G_TYPE_OBJECT); + +G_DEFINE_ENUM_TYPE(AstalWpScale, astal_wp_scale, + G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_LINEAR, "linear"), + G_DEFINE_ENUM_VALUE(ASTAL_WP_SCALE_CUBIC, "cubic")); + +typedef enum { + ASTAL_WP_WP_SIGNAL_ENDPOINT_ADDED, + ASTAL_WP_WP_SIGNAL_ENDPOINT_REMOVED, + ASTAL_WP_WP_SIGNAL_DEVICE_ADDED, + ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED, + ASTAL_WP_WP_N_SIGNALS +} AstalWpWpSignals; + +static guint astal_wp_wp_signals[ASTAL_WP_WP_N_SIGNALS] = { + 0, +}; + +typedef enum { + ASTAL_WP_WP_PROP_AUDIO = 1, + ASTAL_WP_WP_PROP_VIDEO, + ASTAL_WP_WP_PROP_ENDPOINTS, + ASTAL_WP_WP_PROP_DEVICES, + ASTAL_WP_WP_PROP_DEFAULT_SPEAKER, + ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE, + ASTAL_WP_WP_PROP_SCALE, + ASTAL_WP_WP_N_PROPERTIES, +} AstalWpWpProperties; + +static GParamSpec *astal_wp_wp_properties[ASTAL_WP_WP_N_PROPERTIES] = { + NULL, +}; + +/** + * 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; +} + +/** + * astal_wp_wp_get_endpoints: + * @self: the AstalWpWp object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpEndpoint)): a GList containing the + * endpoints + */ +GList *astal_wp_wp_get_endpoints(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + return g_hash_table_get_values(priv->endpoints); +} + +/** + * astal_wp_wp_get_device: + * @self: the AstalWpWp object + * @id: the id of the device + * + * Returns: (transfer none) (nullable): the device with the given id + */ +AstalWpDevice *astal_wp_wp_get_device(AstalWpWp *self, guint id) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + AstalWpDevice *device = g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id)); + return device; +} + +/** + * astal_wp_wp_get_devices: + * @self: the AstalWpWp object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpDevice)): a GList containing the + * devices + */ +GList *astal_wp_wp_get_devices(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + return g_hash_table_get_values(priv->devices); +} + +/** + * astal_wp_wp_get_audio + * + * Returns: (nullable) (transfer none): gets the audio object + */ +AstalWpAudio *astal_wp_wp_get_audio(AstalWpWp *self) { return self->audio; } + +/** + * astal_wp_wp_get_video + * + * Returns: (nullable) (transfer none): gets the video object + */ +AstalWpVideo *astal_wp_wp_get_video(AstalWpWp *self) { return self->video; } + +/** + * astal_wp_wp_get_default_speaker + * + * Returns: (nullable) (transfer none): gets the default speaker object + */ +AstalWpEndpoint *astal_wp_wp_get_default_speaker(AstalWpWp *self) { return self->default_speaker; } + +/** + * astal_wp_wp_get_default_microphone + * + * Returns: (nullable) (transfer none): gets the default microphone object + */ +AstalWpEndpoint *astal_wp_wp_get_default_microphone(AstalWpWp *self) { + return self->default_microphone; +} + +AstalWpScale astal_wp_wp_get_scale(AstalWpWp *self) { return self->scale; } + +void astal_wp_wp_set_scale(AstalWpWp *self, AstalWpScale scale) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + self->scale = scale; + + if (priv->mixer == NULL) return; + + g_object_set(priv->mixer, "scale", self->scale, NULL); + + GHashTableIter iter; + gpointer key, value; + + g_hash_table_iter_init(&iter, priv->endpoints); + while (g_hash_table_iter_next(&iter, &key, &value)) { + AstalWpEndpoint *ep = ASTAL_WP_ENDPOINT(value); + astal_wp_endpoint_update_volume(ep); + } + + astal_wp_endpoint_update_volume(self->default_speaker); + astal_wp_endpoint_update_volume(self->default_microphone); +} + +static void astal_wp_wp_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpWp *self = ASTAL_WP_WP(object); + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + switch (property_id) { + case ASTAL_WP_WP_PROP_AUDIO: + g_value_set_object(value, astal_wp_wp_get_audio(self)); + break; + case ASTAL_WP_WP_PROP_VIDEO: + g_value_set_object(value, astal_wp_wp_get_video(self)); + break; + case ASTAL_WP_WP_PROP_ENDPOINTS: + g_value_set_pointer(value, g_hash_table_get_values(priv->endpoints)); + break; + case ASTAL_WP_WP_PROP_DEVICES: + g_value_set_pointer(value, g_hash_table_get_values(priv->devices)); + break; + case ASTAL_WP_WP_PROP_DEFAULT_SPEAKER: + g_value_set_object(value, self->default_speaker); + break; + case ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE: + g_value_set_object(value, self->default_microphone); + break; + case ASTAL_WP_WP_PROP_SCALE: + g_value_set_enum(value, self->scale); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_wp_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpWp *self = ASTAL_WP_WP(object); + + switch (property_id) { + case ASTAL_WP_WP_PROP_SCALE: + astal_wp_wp_set_scale(self, g_value_get_enum(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +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); + // g_value_unset(&item); + // } + + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + if (WP_IS_NODE(object)) { + WpNode *node = WP_NODE(object); + AstalWpEndpoint *endpoint = + astal_wp_endpoint_create(node, priv->mixer, priv->defaults, self); + + 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_object_notify(G_OBJECT(self), "endpoints"); + } else if (WP_IS_DEVICE(object)) { + WpDevice *node = WP_DEVICE(object); + AstalWpDevice *device = astal_wp_device_create(node); + g_hash_table_insert(priv->devices, GUINT_TO_POINTER(wp_proxy_get_bound_id(WP_PROXY(node))), + device); + g_signal_emit_by_name(self, "device-added", device); + g_object_notify(G_OBJECT(self), "devices"); + } +} + +static void astal_wp_wp_object_removed(AstalWpWp *self, gpointer object) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + if (WP_IS_NODE(object)) { + guint id = wp_proxy_get_bound_id(WP_PROXY(object)); + AstalWpEndpoint *endpoint = + g_object_ref(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_object_notify(G_OBJECT(self), "endpoints"); + g_object_unref(endpoint); + } else if (WP_IS_DEVICE(object)) { + guint id = wp_proxy_get_bound_id(WP_PROXY(object)); + AstalWpDevice *device = + g_object_ref(g_hash_table_lookup(priv->devices, GUINT_TO_POINTER(id))); + g_hash_table_remove(priv->devices, GUINT_TO_POINTER(id)); + + g_signal_emit_by_name(self, "device-removed", device); + g_object_notify(G_OBJECT(self), "devices"); + g_object_unref(device); + } +} + +static void astal_wp_wp_objm_installed(AstalWpWp *self) { + AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); + + astal_wp_endpoint_init_as_default(self->default_speaker, priv->mixer, priv->defaults, + ASTAL_WP_MEDIA_CLASS_AUDIO_SPEAKER, self); + astal_wp_endpoint_init_as_default(self->default_microphone, priv->mixer, priv->defaults, + ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE, 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_object_set(priv->mixer, "scale", self->scale, NULL); + + 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); + + g_clear_object(&self->video); + g_clear_object(&self->audio); + + wp_core_disconnect(priv->core); + g_clear_object(&self->default_speaker); + g_clear_object(&self->default_microphone); + 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); + priv->devices = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + + wp_init(7); + priv->core = wp_core_new(NULL, NULL, NULL); + + if (!wp_core_connect(priv->core)) { + g_critical("could not connect to PipeWire\n"); + return; + } + + priv->obj_manager = wp_object_manager_new(); + wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_NODE, + WP_OBJECT_FEATURES_ALL); + wp_object_manager_request_object_features(priv->obj_manager, WP_TYPE_GLOBAL_PROXY, + WP_OBJECT_FEATURES_ALL); + + 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_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); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", + "Audio/Device", NULL); + + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Video/Sink", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Video/Source", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Output/Video", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_NODE, WP_CONSTRAINT_TYPE_PW_PROPERTY, + "media.class", "=s", "Stream/Input/Video", NULL); + wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_DEVICE, + WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, "media.class", "=s", + "Video/Device", NULL); + // wp_object_manager_add_interest(priv->obj_manager, WP_TYPE_CLIENT, NULL); + + g_signal_connect_swapped(priv->obj_manager, "installed", (GCallback)astal_wp_wp_objm_installed, + self); + + self->default_speaker = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + self->default_microphone = g_object_new(ASTAL_WP_TYPE_ENDPOINT, NULL); + + self->audio = astal_wp_audio_new(self); + self->video = astal_wp_video_new(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; + object_class->get_property = astal_wp_wp_get_property; + object_class->set_property = astal_wp_wp_set_property; + + astal_wp_wp_properties[ASTAL_WP_WP_PROP_AUDIO] = + g_param_spec_object("audio", "audio", "audio", ASTAL_WP_TYPE_AUDIO, G_PARAM_READABLE); + astal_wp_wp_properties[ASTAL_WP_WP_PROP_VIDEO] = + g_param_spec_object("video", "video", "video", ASTAL_WP_TYPE_VIDEO, G_PARAM_READABLE); + /** + * AstalWpWp:scale: (type AstalWpScale) + * + * The scale used for the volume + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_SCALE] = + g_param_spec_enum("scale", "scale", "scale", ASTAL_WP_TYPE_SCALE, ASTAL_WP_SCALE_CUBIC, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT); + + /** + * AstalWpWp:endpoints: (type GList(AstalWpEndpoint)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_ENDPOINTS] = + g_param_spec_pointer("endpoints", "endpoints", "endpoints", G_PARAM_READABLE); + /** + * AstalWpWp:devices: (type GList(AstalWpDevice)) (transfer container) + * + * A list of AstalWpDevice objects + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEVICES] = + g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE); + /** + * AstalWpWp:default-speaker: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_SPEAKER] = + g_param_spec_object("default-speaker", "default-speaker", "default-speaker", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + /** + * AstalWpWp:default-microphone: + * + * The AstalWndpoint object representing the default speaker + */ + astal_wp_wp_properties[ASTAL_WP_WP_PROP_DEFAULT_MICROPHONE] = + g_param_spec_object("default-microphone", "default-microphone", "default-microphone", + ASTAL_WP_TYPE_ENDPOINT, G_PARAM_READABLE); + + g_object_class_install_properties(object_class, ASTAL_WP_WP_N_PROPERTIES, + astal_wp_wp_properties); + + 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_DEVICE_ADDED] = + g_signal_new("device-added", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); + astal_wp_wp_signals[ASTAL_WP_WP_SIGNAL_DEVICE_REMOVED] = + g_signal_new("device-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, + NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_DEVICE); +} |