diff options
-rw-r--r-- | include/astal/wireplumber/audio.h | 3 | ||||
-rw-r--r-- | include/astal/wireplumber/device.h | 24 | ||||
-rw-r--r-- | include/astal/wireplumber/meson.build | 2 | ||||
-rw-r--r-- | include/astal/wireplumber/profile.h | 17 | ||||
-rw-r--r-- | include/astal/wireplumber/wp.h | 3 | ||||
-rw-r--r-- | include/meson.build | 2 | ||||
-rw-r--r-- | include/private/device-private.h | 15 | ||||
-rw-r--r-- | include/private/endpoint-private.h (renamed from include/endpoint-private.h) | 0 | ||||
-rw-r--r-- | src/audio.c | 86 | ||||
-rw-r--r-- | src/device.c | 285 | ||||
-rw-r--r-- | src/endpoint.c | 9 | ||||
-rw-r--r-- | src/meson.build | 2 | ||||
-rw-r--r-- | src/profile.c | 83 | ||||
-rw-r--r-- | src/wireplumber.c | 109 |
14 files changed, 604 insertions, 36 deletions
diff --git a/include/astal/wireplumber/audio.h b/include/astal/wireplumber/audio.h index 5cfa27e..2d54727 100644 --- a/include/astal/wireplumber/audio.h +++ b/include/astal/wireplumber/audio.h @@ -3,6 +3,7 @@ #include <glib-object.h> +#include "device.h" #include "endpoint.h" G_BEGIN_DECLS @@ -16,6 +17,7 @@ AstalWpEndpoint *astal_wp_audio_get_microphone(AstalWpAudio *self, guint id); AstalWpEndpoint *astal_wp_audio_get_recorder(AstalWpAudio *self, guint id); AstalWpEndpoint *astal_wp_audio_get_stream(AstalWpAudio *self, guint id); AstalWpEndpoint *astal_wp_audio_get_endpoint(AstalWpAudio *self, guint id); +AstalWpDevice *astal_wp_audio_get_device(AstalWpAudio *self, guint id); AstalWpEndpoint *astal_wp_audio_get_default_speaker(AstalWpAudio *self); AstalWpEndpoint *astal_wp_audio_get_default_microphone(AstalWpAudio *self); @@ -24,6 +26,7 @@ GList *astal_wp_audio_get_microphones(AstalWpAudio *self); GList *astal_wp_audio_get_speakers(AstalWpAudio *self); GList *astal_wp_audio_get_recorders(AstalWpAudio *self); GList *astal_wp_audio_get_streams(AstalWpAudio *self); +GList *astal_wp_audio_get_devices(AstalWpAudio *self); AstalWpAudio *astal_wp_audio_get_default(); AstalWpAudio *astal_wp_get_default_audio(); diff --git a/include/astal/wireplumber/device.h b/include/astal/wireplumber/device.h new file mode 100644 index 0000000..6e5f6d4 --- /dev/null +++ b/include/astal/wireplumber/device.h @@ -0,0 +1,24 @@ +#ifndef ASTAL_WP_DEVICE_H +#define ASTAL_WP_DEVICE_H + +#include <glib-object.h> + +#include "profile.h" + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_DEVICE (astal_wp_device_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpDevice, astal_wp_device, ASTAL_WP, DEVICE, GObject) + +guint astal_wp_device_get_id(AstalWpDevice *self); +const gchar *astal_wp_device_get_description(AstalWpDevice *self); +const gchar *astal_wp_device_get_icon(AstalWpDevice *self); +AstalWpProfile *astal_wp_device_get_profile(AstalWpDevice *self, gint id); +GList *astal_wp_device_get_profiles(AstalWpDevice *self); +void astal_wp_device_set_active_profile(AstalWpDevice *self, int profile_id); +gint astal_wp_device_get_active_profile(AstalWpDevice *self); + +G_END_DECLS + +#endif // !ASTAL_WP_DEVICE_H diff --git a/include/astal/wireplumber/meson.build b/include/astal/wireplumber/meson.build index 525b380..6cd0147 100644 --- a/include/astal/wireplumber/meson.build +++ b/include/astal/wireplumber/meson.build @@ -1,7 +1,9 @@ astal_wireplumber_subheaders = files( 'wp.h', 'endpoint.h', + 'device.h', 'audio.h', + 'profile.h', ) install_headers(astal_wireplumber_subheaders, subdir : 'astal/wireplumber') diff --git a/include/astal/wireplumber/profile.h b/include/astal/wireplumber/profile.h new file mode 100644 index 0000000..2ec768e --- /dev/null +++ b/include/astal/wireplumber/profile.h @@ -0,0 +1,17 @@ +#ifndef ASTAL_WP_PROFILE_H +#define ASTAL_WP_PROFILE_H + +#include <glib-object.h> + +G_BEGIN_DECLS + +#define ASTAL_WP_TYPE_PROFILE (astal_wp_profile_get_type()) + +G_DECLARE_FINAL_TYPE(AstalWpProfile, astal_wp_profile, ASTAL_WP, PROFILE, GObject) + +gint astal_wp_profile_get_index(AstalWpProfile *self); +const gchar *astal_wp_profile_get_description(AstalWpProfile *self); + +G_END_DECLS + +#endif // !ASTAL_WP_PROFILE_H diff --git a/include/astal/wireplumber/wp.h b/include/astal/wireplumber/wp.h index 94e68cc..3b4b5e5 100644 --- a/include/astal/wireplumber/wp.h +++ b/include/astal/wireplumber/wp.h @@ -4,6 +4,7 @@ #include <glib-object.h> #include "audio.h" +#include "device.h" #include "endpoint.h" G_BEGIN_DECLS @@ -20,6 +21,8 @@ AstalWpEndpoint* astal_wp_wp_get_endpoint(AstalWpWp* self, guint id); GList* astal_wp_wp_get_endpoints(AstalWpWp* self); AstalWpEndpoint* astal_wp_wp_get_default_speaker(AstalWpWp* self); AstalWpEndpoint* astal_wp_wp_get_default_microphone(AstalWpWp* self); +AstalWpDevice* astal_wp_wp_get_device(AstalWpWp* self, guint id); +GList* astal_wp_wp_get_devices(AstalWpWp* self); G_END_DECLS diff --git a/include/meson.build b/include/meson.build index 9d32288..441097c 100644 --- a/include/meson.build +++ b/include/meson.build @@ -1,4 +1,4 @@ -astal_wireplumber_inc = include_directories('.', 'astal/wireplumber') +astal_wireplumber_inc = include_directories('.', 'astal/wireplumber', 'private') astal_wireplumber_headers = files( 'astal-wp.h', ) diff --git a/include/private/device-private.h b/include/private/device-private.h new file mode 100644 index 0000000..e98a7f7 --- /dev/null +++ b/include/private/device-private.h @@ -0,0 +1,15 @@ +#ifndef ASTAL_WP_DEVICE_PRIVATE_H +#define ASTAL_WP_DEVICE_PRIVATE_H + +#include <glib-object.h> +#include <wp/wp.h> + +#include "device.h" + +G_BEGIN_DECLS + +AstalWpDevice *astal_wp_device_create(WpDevice *device); + +G_END_DECLS + +#endif // !ASTAL_WP_DEVICE_PRIATE_H diff --git a/include/endpoint-private.h b/include/private/endpoint-private.h index 765f4b8..765f4b8 100644 --- a/include/endpoint-private.h +++ b/include/private/endpoint-private.h diff --git a/src/audio.c b/src/audio.c index 53224a7..081bb6c 100644 --- a/src/audio.c +++ b/src/audio.c @@ -2,6 +2,7 @@ #include <wp/wp.h> +#include "device.h" #include "endpoint.h" #include "wp.h" @@ -25,6 +26,8 @@ typedef enum { ASTAL_WP_AUDIO_SIGNAL_STREAM_REMOVED, ASTAL_WP_AUDIO_SIGNAL_RECORDER_ADDED, ASTAL_WP_AUDIO_SIGNAL_RECORDER_REMOVED, + ASTAL_WP_AUDIO_SIGNAL_DEVICE_ADDED, + ASTAL_WP_AUDIO_SIGNAL_DEVICE_REMOVED, ASTAL_WP_AUDIO_N_SIGNALS } AstalWpWpSignals; @@ -37,6 +40,7 @@ typedef enum { ASTAL_WP_AUDIO_PROP_SPEAKERS, ASTAL_WP_AUDIO_PROP_STREAMS, ASTAL_WP_AUDIO_PROP_RECORDERS, + ASTAL_WP_AUDIO_PROP_DEVICES, ASTAL_WP_AUDIO_PROP_DEFAULT_SPEAKER, ASTAL_WP_AUDIO_PROP_DEFAULT_MICROPHONE, ASTAL_WP_AUDIO_N_PROPERTIES, @@ -111,6 +115,19 @@ AstalWpEndpoint *astal_wp_audio_get_stream(AstalWpAudio *self, guint id) { } /** + * astal_wp_audio_get_device: + * @self: the AstalWpAudio object + * @id: the id of the device + * + * Returns: (transfer none) (nullable): the device with the given id + */ +AstalWpDevice *astal_wp_audio_get_device(AstalWpAudio *self, guint id) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + + return astal_wp_wp_get_device(priv->wp, id); +} + +/** * astal_wp_audio_get_microphones: * @self: the AstalWpAudio object * @@ -195,6 +212,28 @@ GList *astal_wp_audio_get_streams(AstalWpAudio *self) { } /** + * astal_wp_audio_get_devices: + * @self: the AstalWpAudio object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpDevice)): a GList containing the + * devices + */ +GList *astal_wp_audio_get_devices(AstalWpAudio *self) { + AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); + GList *eps = astal_wp_wp_get_devices(priv->wp); + // GList *mics = NULL; + + // for (GList *l = eps; l != NULL; l = l->next) { + // if (astal_wp_endpoint_get_media_class(l->data) == ASTAL_WP_MEDIA_CLASS_AUDIO_MICROPHONE) { + // mics = g_list_append(mics, l->data); + // } + // } + // g_list_free(eps); + // return mics; + return eps; +} + +/** * astal_wp_audio_get_endpoint: * @self: the AstalWpAudio object * @id: the id of the endpoint @@ -248,6 +287,9 @@ static void astal_wp_audio_get_property(GObject *object, guint property_id, GVal case ASTAL_WP_AUDIO_PROP_DEFAULT_SPEAKER: g_value_set_object(value, astal_wp_audio_get_default_speaker(self)); break; + case ASTAL_WP_AUDIO_PROP_DEVICES: + g_value_set_pointer(value, astal_wp_audio_get_devices(self)); + break; case ASTAL_WP_AUDIO_PROP_DEFAULT_MICROPHONE: g_value_set_object(value, astal_wp_audio_get_default_microphone(self)); break; @@ -257,6 +299,20 @@ static void astal_wp_audio_get_property(GObject *object, guint property_id, GVal } } +static void astal_wp_audio_device_added(AstalWpAudio *self, gpointer object) { + AstalWpDevice *endpoint = ASTAL_WP_DEVICE(object); + g_signal_emit_by_name(self, "device-added", endpoint); + g_object_notify(G_OBJECT(self), "devices"); + g_signal_emit_by_name(self, "changed"); +} + +static void astal_wp_audio_device_removed(AstalWpAudio *self, gpointer object) { + AstalWpDevice *endpoint = ASTAL_WP_DEVICE(object); + g_signal_emit_by_name(self, "device-removed", endpoint); + g_object_notify(G_OBJECT(self), "devices"); + g_signal_emit_by_name(self, "changed"); +} + static void astal_wp_audio_object_added(AstalWpAudio *self, gpointer object) { AstalWpEndpoint *endpoint = ASTAL_WP_ENDPOINT(object); switch (astal_wp_endpoint_get_media_class(endpoint)) { @@ -325,16 +381,6 @@ AstalWpAudio *astal_wp_audio_get_default() { */ AstalWpAudio *astal_wp_get_default_audio() { return astal_wp_audio_get_default(); } -static void astal_wp_audio_dispose(GObject *object) { - AstalWpAudio *self = ASTAL_WP_AUDIO(object); - AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); -} - -static void astal_wp_audio_finalize(GObject *object) { - AstalWpAudio *self = ASTAL_WP_AUDIO(object); - AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); -} - static void astal_wp_audio_init(AstalWpAudio *self) { AstalWpAudioPrivate *priv = astal_wp_audio_get_instance_private(self); @@ -344,12 +390,14 @@ static void astal_wp_audio_init(AstalWpAudio *self) { self); g_signal_connect_swapped(priv->wp, "endpoint-removed", G_CALLBACK(astal_wp_audio_object_removed), self); + g_signal_connect_swapped(priv->wp, "device-added", G_CALLBACK(astal_wp_audio_device_added), + self); + g_signal_connect_swapped(priv->wp, "device-removed", G_CALLBACK(astal_wp_audio_device_removed), + self); } static void astal_wp_audio_class_init(AstalWpAudioClass *class) { GObjectClass *object_class = G_OBJECT_CLASS(class); - object_class->finalize = astal_wp_audio_finalize; - object_class->dispose = astal_wp_audio_dispose; object_class->get_property = astal_wp_audio_get_property; /** @@ -381,6 +429,13 @@ static void astal_wp_audio_class_init(AstalWpAudioClass *class) { astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_STREAMS] = g_param_spec_pointer("streams", "streams", "streams", G_PARAM_READABLE); /** + * AstalWpAudio:devices: (type GList(AstalWpDevice)) (transfer container) + * + * A list of AstalWpEndpoint objects + */ + astal_wp_audio_properties[ASTAL_WP_AUDIO_PROP_DEVICES] = + g_param_spec_pointer("devices", "devices", "devices", G_PARAM_READABLE); + /** * AstalWpAudio:default-speaker: * * The AstalWndpoint object representing the default speaker @@ -424,6 +479,13 @@ static void astal_wp_audio_class_init(AstalWpAudioClass *class) { astal_wp_audio_signals[ASTAL_WP_AUDIO_SIGNAL_RECORDER_REMOVED] = g_signal_new("recorder-removed", G_TYPE_FROM_CLASS(class), G_SIGNAL_RUN_FIRST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, ASTAL_WP_TYPE_ENDPOINT); + astal_wp_audio_signals[ASTAL_WP_AUDIO_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_audio_signals[ASTAL_WP_AUDIO_SIGNAL_MICROPHONE_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); + astal_wp_audio_signals[ASTAL_WP_AUDIO_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/device.c b/src/device.c new file mode 100644 index 0000000..7b6629f --- /dev/null +++ b/src/device.c @@ -0,0 +1,285 @@ +#include "device-private.h" + +#include <wp/wp.h> +#include "profile.h" + + +struct _AstalWpDevice { + GObject parent_instance; + + guint id; + gchar *description; + gchar *icon; + gint active_profile; +}; + +typedef struct { + WpDevice *device; + GHashTable *profiles; +} AstalWpDevicePrivate; + +G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpDevice, astal_wp_device, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_DEVICE_PROP_ID = 1, + ASTAL_WP_DEVICE_PROP_DESCRIPTION, + ASTAL_WP_DEVICE_PROP_ICON, + ASTAL_WP_DEVICE_PROP_PROFILES, + ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE, + ASTAL_WP_DEVICE_N_PROPERTIES, +} AstalWpDeviceProperties; + +typedef enum { ASTAL_WP_DEVICE_SIGNAL_CHANGED, ASTAL_WP_DEVICE_N_SIGNALS } AstalWpDeviceSignals; + +static guint astal_wp_device_signals[ASTAL_WP_DEVICE_N_SIGNALS] = { + 0, +}; +static GParamSpec *astal_wp_device_properties[ASTAL_WP_DEVICE_N_PROPERTIES] = { + NULL, +}; + +guint astal_wp_device_get_id(AstalWpDevice *self) { return self->id; } + +const gchar *astal_wp_device_get_description(AstalWpDevice *self) { return self->description; } + +const gchar *astal_wp_device_get_icon(AstalWpDevice *self) { return self->icon; } + +gint astal_wp_device_get_active_profile(AstalWpDevice *self) { return self->active_profile; } + +void astal_wp_device_set_active_profile(AstalWpDevice *self, int profile_id) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + WpSpaPodBuilder *builder = + wp_spa_pod_builder_new_object("Spa:Pod:Object:Param:Profile", "Profile"); + wp_spa_pod_builder_add_property(builder, "index"); + wp_spa_pod_builder_add_int(builder, profile_id); + WpSpaPod *pod = wp_spa_pod_builder_end(builder); + wp_pipewire_object_set_param(WP_PIPEWIRE_OBJECT(priv->device), "Profile", 0, pod); + + wp_spa_pod_unref(pod); + wp_spa_pod_builder_unref(builder); +} + +/** + * astal_wp_device_get_profile: + * @self: the AstalWpDevice object + * @id: the id of the profile + * + * Returns: (transfer none) (nullable): the profile with the given id + */ +AstalWpProfile *astal_wp_device_get_profile(AstalWpDevice *self, gint id) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + return g_hash_table_lookup(priv->profiles, GINT_TO_POINTER(id)); +} + +/** + * astal_wp_device_get_profiles: + * @self: the AstalWpDevice object + * + * Returns: (transfer container) (nullable) (type GList(AstalWpProfile)): a GList containing the + * profiles + */ +GList *astal_wp_device_get_profiles(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + return g_hash_table_get_values(priv->profiles); +} + +static void astal_wp_device_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + + switch (property_id) { + case ASTAL_WP_DEVICE_PROP_ID: + g_value_set_uint(value, self->id); + break; + case ASTAL_WP_DEVICE_PROP_DESCRIPTION: + g_value_set_string(value, self->description); + break; + case ASTAL_WP_DEVICE_PROP_ICON: + g_value_set_string(value, self->icon); + break; + case ASTAL_WP_DEVICE_PROP_PROFILES: + g_value_set_pointer(value, astal_wp_device_get_profiles(self)); + break; + case ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE: + g_value_set_int(value, self->active_profile); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_device_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + + switch (property_id) { + case ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE: + astal_wp_device_set_active_profile(self, g_value_get_int(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_device_update_profiles(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + g_hash_table_remove_all(priv->profiles); + + WpIterator *iter = + wp_pipewire_object_enum_params_sync(WP_PIPEWIRE_OBJECT(priv->device), "EnumProfile", NULL); + GValue profile = G_VALUE_INIT; + while (wp_iterator_next(iter, &profile)) { + WpSpaPod *pod = g_value_get_boxed(&profile); + + gint index; + gchar *description; + wp_spa_pod_get_object(pod, NULL, "index", "i", &index, "description", "s", &description, + NULL); + + g_hash_table_insert( + priv->profiles, GINT_TO_POINTER(index), + g_object_new(ASTAL_WP_TYPE_PROFILE, "index", index, "description", description, NULL)); + } + wp_iterator_unref(iter); + + g_object_notify(G_OBJECT(self), "profiles"); +} + +static void astal_wp_device_update_active_profile(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + WpIterator *iter = + wp_pipewire_object_enum_params_sync(WP_PIPEWIRE_OBJECT(priv->device), "Profile", NULL); + GValue profile = G_VALUE_INIT; + while (wp_iterator_next(iter, &profile)) { + WpSpaPod *pod = g_value_get_boxed(&profile); + + gint index; + gchar *description; + wp_spa_pod_get_object(pod, NULL, "index", "i", &index, "description", "s", &description, + NULL); + + g_hash_table_insert( + priv->profiles, GINT_TO_POINTER(index), + g_object_new(ASTAL_WP_TYPE_PROFILE, "index", index, "description", description, NULL)); + + self->active_profile = index; + } + wp_iterator_unref(iter); + + g_object_notify(G_OBJECT(self), "active-profile-id"); +} + +static void astal_wp_device_params_changed(AstalWpDevice *self, const gchar *prop) { + if (g_strcmp0(prop, "EnumProfile") == 0) { + astal_wp_device_update_profiles(self); + } else if (g_strcmp0(prop, "Profile") == 0) { + astal_wp_device_update_active_profile(self); + } +} + +static void astal_wp_device_update_properties(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + if (priv->device == NULL) return; + self->id = wp_proxy_get_bound_id(WP_PROXY(priv->device)); + + const gchar *description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.description"); + if (description == NULL) { + description = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.name"); + } + if (description == NULL) { + description = "unknown"; + } + g_free(self->description); + self->description = g_strdup(description); + + const gchar *icon = + wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->device), "device.icon-name"); + if (description == NULL) { + icon = "soundcard-symbolic"; + } + g_free(self->icon); + self->icon = g_strdup(icon); + + astal_wp_device_update_profiles(self); + astal_wp_device_update_active_profile(self); + + g_object_notify(G_OBJECT(self), "id"); + g_object_notify(G_OBJECT(self), "icon"); + g_object_notify(G_OBJECT(self), "description"); + g_signal_emit_by_name(self, "changed"); +} + +AstalWpDevice *astal_wp_device_create(WpDevice *device) { + AstalWpDevice *self = g_object_new(ASTAL_WP_TYPE_DEVICE, NULL); + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + priv->device = g_object_ref(device); + + g_signal_connect_swapped(priv->device, "params-changed", + G_CALLBACK(astal_wp_device_params_changed), self); + + astal_wp_device_update_properties(self); + return self; +} + +static void astal_wp_device_init(AstalWpDevice *self) { + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + priv->device = NULL; + + priv->profiles = g_hash_table_new_full(g_direct_hash, g_direct_equal, NULL, g_object_unref); + + self->description = NULL; + self->icon = NULL; +} + +static void astal_wp_device_dispose(GObject *object) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + AstalWpDevicePrivate *priv = astal_wp_device_get_instance_private(self); + + g_clear_object(&priv->device); +} + +static void astal_wp_device_finalize(GObject *object) { + AstalWpDevice *self = ASTAL_WP_DEVICE(object); + g_free(self->description); + g_free(self->icon); +} + +static void astal_wp_device_class_init(AstalWpDeviceClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->dispose = astal_wp_device_dispose; + object_class->finalize = astal_wp_device_finalize; + object_class->get_property = astal_wp_device_get_property; + object_class->set_property = astal_wp_device_set_property; + + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ID] = + g_param_spec_uint("id", "id", "id", 0, UINT_MAX, 0, G_PARAM_READABLE); + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, G_PARAM_READABLE); + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ICON] = + g_param_spec_string("icon", "icon", "icon", NULL, G_PARAM_READABLE); + /** + * AstalWpDevice:profiles: (type GList(AstalWpProfile)) (transfer container) + * + * A list of AstalWpProfile objects + */ + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_PROFILES] = + g_param_spec_pointer("profiles", "profiles", "profiles", G_PARAM_READABLE); + astal_wp_device_properties[ASTAL_WP_DEVICE_PROP_ACTIVE_PROFILE] = + g_param_spec_int("active-profile-id", "active-profile-id", "active-profile-id", G_MININT, + G_MAXINT, 0, G_PARAM_READWRITE); + + g_object_class_install_properties(object_class, ASTAL_WP_DEVICE_N_PROPERTIES, + astal_wp_device_properties); + + astal_wp_device_signals[ASTAL_WP_DEVICE_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/endpoint.c b/src/endpoint.c index 7d7a275..d6f4ccb 100644 --- a/src/endpoint.c +++ b/src/endpoint.c @@ -203,8 +203,9 @@ static void astal_wp_endpoint_update_properties(AstalWpEndpoint *self) { const gchar *type = wp_pipewire_object_get_property(WP_PIPEWIRE_OBJECT(priv->node), "media.class"); - const GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); - self->type = g_enum_get_value_by_nick(enum_class, type)->value; + GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); + if (g_enum_get_value_by_nick(enum_class, type) != NULL) + self->type = g_enum_get_value_by_nick(enum_class, type)->value; g_type_class_unref(enum_class); g_object_notify(G_OBJECT(self), "id"); @@ -216,7 +217,7 @@ static void astal_wp_endpoint_update_properties(AstalWpEndpoint *self) { static void astal_wp_endpoint_default_changed_as_default(AstalWpEndpoint *self) { AstalWpEndpointPrivate *priv = astal_wp_endpoint_get_instance_private(self); - const GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); + GEnumClass *enum_class = g_type_class_ref(ASTAL_WP_TYPE_MEDIA_CLASS); const gchar *media_class = g_enum_get_value(enum_class, priv->media_class)->value_nick; guint defaultId; g_signal_emit_by_name(priv->defaults, "get-default-node", media_class, &defaultId); @@ -318,8 +319,6 @@ static void astal_wp_endpoint_dispose(GObject *object) { g_signal_handler_disconnect(priv->defaults, priv->default_signal_handler_id); g_signal_handler_disconnect(priv->mixer, priv->mixer_signal_handler_id); - g_print("dispose: id: %u, name: %s\n", self->id, self->description); - g_clear_object(&priv->node); g_clear_object(&priv->mixer); g_clear_object(&priv->defaults); diff --git a/src/meson.build b/src/meson.build index ae56c6f..ce27c3d 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,8 @@ srcs = files( 'wireplumber.c', 'endpoint.c', + 'device.c', + 'profile.c', 'audio.c', ) diff --git a/src/profile.c b/src/profile.c new file mode 100644 index 0000000..cb32240 --- /dev/null +++ b/src/profile.c @@ -0,0 +1,83 @@ +#include <wp/wp.h> +#include "profile.h" + +struct _AstalWpProfile { + GObject parent_instance; + + gint index; + gchar *description; +}; + +G_DEFINE_FINAL_TYPE(AstalWpProfile, astal_wp_profile, G_TYPE_OBJECT); + +typedef enum { + ASTAL_WP_PROFILE_PROP_INDEX = 1, + ASTAL_WP_PROFILE_PROP_DESCRIPTION, + ASTAL_WP_PROFILE_N_PROPERTIES, +} AstalWpProfileProperties; + +static GParamSpec *astal_wp_profile_properties[ASTAL_WP_PROFILE_N_PROPERTIES] = { + NULL, +}; + +gint astal_wp_profile_get_index(AstalWpProfile *self) { return self->index; } + +const gchar *astal_wp_profile_get_description(AstalWpProfile *self) { return self->description; } + +static void astal_wp_profile_get_property(GObject *object, guint property_id, GValue *value, + GParamSpec *pspec) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + + switch (property_id) { + case ASTAL_WP_PROFILE_PROP_INDEX: + g_value_set_int(value, self->index); + break; + case ASTAL_WP_PROFILE_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_profile_set_property(GObject *object, guint property_id, const GValue *value, + GParamSpec *pspec) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + + switch (property_id) { + case ASTAL_WP_PROFILE_PROP_INDEX: + self->index = g_value_get_int(value); + break; + case ASTAL_WP_PROFILE_PROP_DESCRIPTION: + g_free(self->description); + self->description = g_strdup(g_value_get_string(value)); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID(object, property_id, pspec); + break; + } +} + +static void astal_wp_profile_init(AstalWpProfile *self) { self->description = NULL; } + +static void astal_wp_profile_finalize(GObject *object) { + AstalWpProfile *self = ASTAL_WP_PROFILE(object); + g_free(self->description); +} + +static void astal_wp_profile_class_init(AstalWpProfileClass *class) { + GObjectClass *object_class = G_OBJECT_CLASS(class); + object_class->finalize = astal_wp_profile_finalize; + object_class->get_property = astal_wp_profile_get_property; + object_class->set_property = astal_wp_profile_set_property; + + astal_wp_profile_properties[ASTAL_WP_PROFILE_PROP_DESCRIPTION] = + g_param_spec_string("description", "description", "description", NULL, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + astal_wp_profile_properties[ASTAL_WP_PROFILE_PROP_INDEX] = + g_param_spec_int("index", "index", "index", G_MININT, G_MAXINT, 0, + G_PARAM_READWRITE | G_PARAM_CONSTRUCT_ONLY); + g_object_class_install_properties(object_class, ASTAL_WP_PROFILE_N_PROPERTIES, + astal_wp_profile_properties); +} diff --git a/src/wireplumber.c b/src/wireplumber.c index 6bf712a..527f020 100644 --- a/src/wireplumber.c +++ b/src/wireplumber.c @@ -1,6 +1,7 @@ #include <wp/wp.h> #include "audio.h" +#include "device-private.h" #include "endpoint-private.h" #include "wp.h" @@ -20,6 +21,7 @@ typedef struct { gint pending_plugins; GHashTable *endpoints; + GHashTable *devices; } AstalWpWpPrivate; G_DEFINE_FINAL_TYPE_WITH_PRIVATE(AstalWpWp, astal_wp_wp, G_TYPE_OBJECT); @@ -28,6 +30,8 @@ typedef enum { ASTAL_WP_WP_SIGNAL_CHANGED, 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; @@ -38,6 +42,7 @@ static guint astal_wp_wp_signals[ASTAL_WP_WP_N_SIGNALS] = { typedef enum { ASTAL_WP_WP_PROP_AUDIO = 1, 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_N_PROPERTIES, @@ -74,6 +79,32 @@ GList *astal_wp_wp_get_endpoints(AstalWpWp *self) { } /** + * 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 @@ -108,6 +139,9 @@ static void astal_wp_wp_get_property(GObject *object, guint property_id, GValue 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; @@ -136,30 +170,49 @@ static void astal_wp_wp_object_added(AstalWpWp *self, gpointer object) { AstalWpWpPrivate *priv = astal_wp_wp_get_instance_private(self); - WpNode *node = WP_NODE(object); - AstalWpEndpoint *endpoint = astal_wp_endpoint_create(node, priv->mixer, priv->defaults); - - 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"); + if (WP_IS_NODE(object)) { + WpNode *node = WP_NODE(object); + AstalWpEndpoint *endpoint = astal_wp_endpoint_create(node, priv->mixer, priv->defaults); + + 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(device))), device); + g_signal_emit_by_name(self, "device-added", device); + g_object_notify(G_OBJECT(self), "devices"); + } 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_object_notify(G_OBJECT(self), "endpoints"); + 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); + } g_signal_emit_by_name(self, "changed"); } @@ -261,6 +314,7 @@ 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(WP_INIT_ALL); priv->core = wp_core_new(NULL, NULL, NULL); @@ -272,6 +326,8 @@ static void astal_wp_wp_init(AstalWpWp *self) { 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, @@ -280,6 +336,10 @@ static void astal_wp_wp_init(AstalWpWp *self) { "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_CLIENT, NULL); g_signal_connect_swapped(priv->obj_manager, "installed", (GCallback)astal_wp_wp_objm_installed, self); @@ -312,6 +372,13 @@ static void astal_wp_wp_class_init(AstalWpWpClass *class) { 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 @@ -337,6 +404,12 @@ static void astal_wp_wp_class_init(AstalWpWpClass *class) { 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); 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); |