/* * Copyright © 2019 Collabora Ltd. * @author George Kiagiadakis * * SPDX-License-Identifier: MIT */ #include "audiomixer.h" #include #include struct audiomixer { WpCore *core; GMainLoop *loop; GMainContext *context; GThread *thread; GMutex lock; GCond cond; GPtrArray *mixer_controls; gint initialized; WpObjectManager *om; WpPlugin *default_nodes_api; WpPlugin *mixer_api; const struct audiomixer_events *events; void *events_data; }; struct mixer_control_impl { struct mixer_control pub; guint32 node_id; }; struct action { struct audiomixer *audiomixer; union { struct { guint32 id; gfloat volume; } change_volume; struct { guint32 id; gboolean mute; } change_mute; }; }; static gboolean get_mixer_controls (struct audiomixer * self, guint32 node_id, gdouble * vol, gboolean * mute) { g_autoptr (GVariant) v = NULL; g_signal_emit_by_name (self->mixer_api, "get-volume", node_id, &v); return v && g_variant_lookup (v, "volume", "d", &vol, NULL) && g_variant_lookup (v, "mute", "b", &mute, NULL); } static void add_control (struct audiomixer *self, const char *name, guint32 node_id) { struct mixer_control_impl *mixctl = NULL; gdouble volume = 1.0; gboolean mute = FALSE; /* get current values */ if (!get_mixer_controls (self, node_id, &volume, &mute)) { g_warning ("failed to get object controls when populating controls"); return; } /* create the control */ mixctl = g_new0 (struct mixer_control_impl, 1); snprintf (mixctl->pub.name, sizeof (mixctl->pub.name), "%s", name); mixctl->pub.volume = volume; mixctl->pub.mute = mute; mixctl->node_id = node_id; g_ptr_array_add (self->mixer_controls, mixctl); g_debug ("added control %s", mixctl->pub.name); } static void volume_changed (struct audiomixer * self, guint32 node_id) { g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); gdouble vol = 1.0; gboolean mute = FALSE; for (guint i = 0; i < self->mixer_controls->len; i++) { struct mixer_control_impl *ctl; guint change_mask = 0; ctl = g_ptr_array_index (self->mixer_controls, i); if (ctl->node_id != node_id) continue; if (!get_mixer_controls (self, node_id, &vol, &mute)) { g_warning ("failed to get object controls when volume changed"); return; } if ((ctl->pub.volume - 0.01f) > vol || (ctl->pub.volume + 0.01f) < vol) { ctl->pub.volume = vol; change_mask |= MIXER_CONTROL_CHANGE_FLAG_VOLUME; } if (ctl->pub.mute != mute) { ctl->pub.mute = mute; change_mask |= MIXER_CONTROL_CHANGE_FLAG_MUTE; } if (self->events && self->events->value_changed) self->events->value_changed (self->events_data, change_mask, &ctl->pub); break; } } static void rescan_controls (struct audiomixer * self) { g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); g_autoptr (WpIterator) it = NULL; g_auto (GValue) val = G_VALUE_INIT; guint32 id = -1; g_debug ("rescan"); /* clear previous */ g_ptr_array_set_size (self->mixer_controls, 0); /* add master controls */ g_signal_emit_by_name (self->default_nodes_api, "get-default-node", "Audio/Sink", &id); if (id != (guint32)-1) add_control (self, "Master Playback", id); g_signal_emit_by_name (self->default_nodes_api, "get-default-node", "Audio/Source", &id); if (id != (guint32)-1) add_control (self, "Master Capture", id); /* add endpoints */ it = wp_object_manager_new_iterator (self->om); for (; wp_iterator_next (it, &val); g_value_unset (&val)) { WpPipewireObject *ep = g_value_get_object (&val); const gchar *name = wp_pipewire_object_get_property (ep, "endpoint.description"); const gchar *node = wp_pipewire_object_get_property (ep, "node.id"); id = node ? atoi(node) : 0; if (name && id != 0 && id != (guint32)-1) add_control (self, name, id); } /* notify subscribers */ if (self->events && self->events->controls_changed) self->events->controls_changed (self->events_data); g_cond_broadcast (&self->cond); } static void on_default_nodes_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) { g_autoptr (GError) error = NULL; if (!wp_object_activate_finish (p, res, &error)) { g_warning ("%s", error->message); } if (wp_object_get_active_features (WP_OBJECT (self->mixer_api)) & WP_PLUGIN_FEATURE_ENABLED) wp_core_install_object_manager (self->core, self->om); g_signal_connect_swapped (self->default_nodes_api, "changed", (GCallback) rescan_controls, self); } static void on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) { g_autoptr (GError) error = NULL; if (!wp_object_activate_finish (p, res, &error)) { g_warning ("%s", error->message); } if (wp_object_get_active_features (WP_OBJECT (self->default_nodes_api)) & WP_PLUGIN_FEATURE_ENABLED) wp_core_install_object_manager (self->core, self->om); g_signal_connect_swapped (self->mixer_api, "changed", (GCallback) volume_changed, self); } static void on_core_connected (struct audiomixer * self) { self->om = wp_object_manager_new (); wp_object_manager_add_interest (self->om, WP_TYPE_ENDPOINT, WP_CONSTRAINT_TYPE_PW_GLOBAL_PROPERTY, PW_KEY_MEDIA_CLASS, "#s", "Audio/*", NULL); wp_object_manager_request_object_features (self->om, WP_TYPE_ENDPOINT, WP_OBJECT_FEATURES_ALL); g_signal_connect_swapped (self->om, "objects-changed", (GCallback) rescan_controls, self); wp_object_activate (WP_OBJECT (self->default_nodes_api), WP_PLUGIN_FEATURE_ENABLED, NULL, (GAsyncReadyCallback) on_default_nodes_activated, self); wp_object_activate (WP_OBJECT (self->mixer_api), WP_PLUGIN_FEATURE_ENABLED, NULL, (GAsyncReadyCallback) on_mixer_activated, self); } static void on_core_disconnected (struct audiomixer * self) { g_ptr_array_set_size (self->mixer_controls, 0); g_clear_object (&self->om); g_signal_handlers_disconnect_by_data (self->default_nodes_api, self); g_signal_handlers_disconnect_by_data (self->mixer_api, self); wp_object_deactivate (WP_OBJECT (self->default_nodes_api), WP_PLUGIN_FEATURE_ENABLED); wp_object_deactivate (WP_OBJECT (self->mixer_api), WP_PLUGIN_FEATURE_ENABLED); } static void audiomixer_init_in_thread (struct audiomixer * self) { g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); g_autoptr (GError) error = NULL; self->context = g_main_context_new (); g_main_context_push_thread_default (s