diff options
Diffstat (limited to 'binding/audiomixer.c')
-rw-r--r-- | binding/audiomixer.c | 482 |
1 files changed, 226 insertions, 256 deletions
diff --git a/binding/audiomixer.c b/binding/audiomixer.c index aceaefe..78dd347 100644 --- a/binding/audiomixer.c +++ b/binding/audiomixer.c @@ -18,13 +18,12 @@ struct audiomixer GMutex lock; GCond cond; - WpRemoteState state; GPtrArray *mixer_controls; - GWeakRef session; - /* ref held in the thread function */ - WpObjectManager *sessions_om; - WpObjectManager *endpoints_om; + gint initialized; + WpObjectManager *om; + WpPlugin *default_nodes_api; + WpPlugin *mixer_api; const struct audiomixer_events *events; void *events_data; @@ -33,9 +32,7 @@ struct audiomixer struct mixer_control_impl { struct mixer_control pub; - gboolean is_master; - guint32 id; - WpEndpoint *endpoint; + guint32 node_id; }; struct action @@ -53,35 +50,67 @@ struct action }; }; +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 -control_changed (WpEndpoint * ep, guint32 control_id, struct audiomixer * self) +volume_changed (struct audiomixer * self, guint32 node_id) { - struct mixer_control_impl *ctl; + 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->endpoint != ep) + if (ctl->node_id != node_id) continue; - guint change_mask = 0; + if (!get_mixer_controls (self, node_id, &vol, &mute)) { + g_warning ("failed to get object controls when volume changed"); + return; + } - switch (control_id) { - case WP_ENDPOINT_CONTROL_VOLUME: { - float vol; - wp_endpoint_get_control_float (ep, control_id, &vol); + if ((ctl->pub.volume - 0.01f) > vol || (ctl->pub.volume + 0.01f) < vol) { ctl->pub.volume = vol; - change_mask = MIXER_CONTROL_CHANGE_FLAG_VOLUME; - break; + change_mask |= MIXER_CONTROL_CHANGE_FLAG_VOLUME; } - case WP_ENDPOINT_CONTROL_MUTE: { - gboolean mute; - wp_endpoint_get_control_boolean (ep, control_id, &mute); + if (ctl->pub.mute != mute) { ctl->pub.mute = mute; - change_mask = MIXER_CONTROL_CHANGE_FLAG_MUTE; - break; - } - default: - break; + change_mask |= MIXER_CONTROL_CHANGE_FLAG_MUTE; } if (self->events && self->events->value_changed) @@ -90,239 +119,190 @@ control_changed (WpEndpoint * ep, guint32 control_id, struct audiomixer * self) } } -/* called with self->lock locked */ static void -check_and_populate_controls (struct audiomixer * self) +rescan_controls (struct audiomixer * self) { - g_autoptr (WpSession) session = NULL; - g_autoptr (WpEndpoint) def_ep = NULL; - guint32 def_id = 0; - struct mixer_control_impl *master_ctl = NULL; - - /* find the session */ - session = g_weak_ref_get (&self->session); - - /* find the default audio sink endpoint */ - if (session) { - g_autoptr (GPtrArray) arr = - wp_object_manager_get_objects (self->endpoints_om, 0); - def_id = wp_session_get_default_endpoint (session, - WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK); - - for (guint i = 0; i < arr->len; i++) { - WpEndpoint *ep = g_ptr_array_index (arr, i); - guint32 id = wp_proxy_get_global_id (WP_PROXY (ep)); - if (id != def_id) - continue; - def_ep = g_object_ref (ep); - } + 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); } - /* find the audio sink endpoint that was the default before */ - for (guint i = 0; i < self->mixer_controls->len; i++) { - struct mixer_control_impl *ctl = (struct mixer_control_impl *) - g_ptr_array_index (self->mixer_controls, i); - if (ctl->is_master) { - master_ctl = ctl; - break; - } - } + /* notify subscribers */ + if (self->events && self->events->controls_changed) + self->events->controls_changed (self->events_data); + g_cond_broadcast (&self->cond); +} - g_debug ("check_and_populate: session:%p, def_ep:%p, def_id:%u, " - "master_ctl:%p, master_id:%u", session, def_ep, def_id, - master_ctl, master_ctl ? master_ctl->id : 0); - - /* case 1: there is a default endpoint but it doesn't match - what we currently expose -> clear and expose the new one */ - if (def_ep && (!master_ctl || master_ctl->id != def_id)) { - struct mixer_control_impl *mixctl = NULL; - gfloat volume; - gboolean mute; - - /* clear previous */ - if (master_ctl) - g_signal_handlers_disconnect_by_data (master_ctl->endpoint, self); - g_ptr_array_set_size (self->mixer_controls, 0); - - /* get current master values */ - wp_endpoint_get_control_float (def_ep, WP_ENDPOINT_CONTROL_VOLUME, &volume); - wp_endpoint_get_control_boolean (def_ep, WP_ENDPOINT_CONTROL_MUTE, &mute); - - /* create the Master control */ - mixctl = g_new0 (struct mixer_control_impl, 1); - strncpy (mixctl->pub.name, "Master", sizeof (mixctl->pub.name)); - mixctl->pub.volume = volume; - mixctl->pub.mute = mute; - mixctl->is_master = TRUE; - mixctl->id = def_id; - mixctl->endpoint = g_object_ref (def_ep); - g_ptr_array_add (self->mixer_controls, mixctl); - - /* track changes */ - g_signal_connect (def_ep, "control-changed", (GCallback) control_changed, - self); - - /* wake up audiomixer_ensure_controls() */ - g_cond_broadcast (&self->cond); - - g_debug ("controls changed"); - - /* notify subscribers */ - if (self->events && self->events->controls_changed) - self->events->controls_changed (self->events_data); +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); } - /* case 2: there is no default endpoint but something is exposed -> clear */ - else if (!def_ep && master_ctl) { - g_signal_handlers_disconnect_by_data (master_ctl->endpoint, self); - g_ptr_array_set_size (self->mixer_controls, 0); - g_debug ("controls cleared"); + if (wp_object_get_active_features (WP_OBJECT (self->mixer_api)) + & WP_PLUGIN_FEATURE_ENABLED) + wp_core_install_object_manager (self->core, self->om); - /* notify subscribers */ - if (self->events && self->events->controls_changed) - self->events->controls_changed (self->events_data); - } + g_signal_connect_swapped (self->default_nodes_api, "changed", + (GCallback) rescan_controls, self); } static void -default_endpoint_changed (WpSession * session, WpDefaultEndpointType type, - guint32 new_id, struct audiomixer * self) +on_mixer_activated (WpObject * p, GAsyncResult * res, struct audiomixer * self) { - if (type != WP_DEFAULT_ENDPOINT_TYPE_AUDIO_SINK) - return; + g_autoptr (GError) error = NULL; + if (!wp_object_activate_finish (p, res, &error)) { + g_warning ("%s", error->message); + } - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - check_and_populate_controls (self); + 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 -sessions_changed (WpObjectManager * sessions_om, struct audiomixer * self) +on_core_connected (struct audiomixer * self) { - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - g_autoptr (WpSession) old_session = NULL; - g_autoptr (WpSession) session = NULL; - g_autoptr (GPtrArray) arr = NULL; - - old_session = g_weak_ref_get (&self->session); - - /* normally there is only 1 session */ - arr = wp_object_manager_get_objects (sessions_om, 0); - if (arr->len > 0) - session = WP_SESSION (g_object_ref (g_ptr_array_index (arr, 0))); - - g_debug ("sessions changed, count:%d, session:%p, old_session:%p", - arr->len, session, old_session); - - if (session != old_session) { - if (old_session) - g_signal_handlers_disconnect_by_data (old_session, self); - if (session) - g_signal_connect (session, "default-endpoint-changed", - (GCallback) default_endpoint_changed, self); - - g_weak_ref_set (&self->session, session); - } - - check_and_populate_controls (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 -endpoints_changed (WpObjectManager * endpoints_om, struct audiomixer * self) +on_core_disconnected (struct audiomixer * self) { - g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - check_and_populate_controls (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 -remote_state_changed (WpCore *core, WpRemoteState state, - struct audiomixer * self) +audiomixer_init_in_thread (struct audiomixer * self) { g_autoptr (GMutexLocker) locker = g_mutex_locker_new (&self->lock); - self->state = state; - g_cond_broadcast (&self->cond); -} + g_autoptr (GError) error = NULL; -static gboolean -connect_in_idle (struct audiomixer * self) -{ - wp_core_connect (self->core); - return G_SOURCE_REMOVE; + self->context = g_main_context_new (); + g_main_context_push_thread_default (self->context); + + self->loop = g_main_loop_new (self->context, FALSE); + self->core = wp_core_new (self->context, NULL); + + /* load required API modules */ + if (!wp_core_load_component (self->core, + "libwireplumber-module-default-nodes-api", "module", NULL, &error)) { + g_warning ("%s", error->message); + self->initialized = -1; + goto out; + } + if (!wp_core_load_component (self->core, + "libwireplumber-module-mixer-api", "module", NULL, &error)) { + g_warning ("%s", error->message); + self->initialized = -1; + goto out; + } + + self->default_nodes_api = wp_plugin_find (self->core, "default-nodes-api"); + self->mixer_api = wp_plugin_find (self->core, "mixer-api"); + g_object_set (G_OBJECT (self->mixer_api), "scale", 1 /* cubic */, NULL); + + g_signal_connect_swapped (self->core, "connected", + G_CALLBACK (on_core_connected), self); + g_signal_connect_swapped (self->core, "disconnected", + G_CALLBACK (on_core_disconnected), self); + + self->initialized = 1; + +out: + g_cond_broadcast (&self->cond); } static void * audiomixer_thread (struct audiomixer * self) { - g_autoptr (WpObjectManager) sessions_om = - self->sessions_om = wp_object_manager_new (); - g_autoptr (WpObjectManager) endpoints_om = - self->endpoints_om = wp_object_manager_new (); - GVariantBuilder b; + audiomixer_init_in_thread (self); - g_main_context_push_thread_default (self->context); + /* main loop for the thread; quits only when audiomixer_free() is called */ + g_main_loop_run (self->loop); - /* install object manager for sessions */ - wp_object_manager_add_proxy_interest (sessions_om, PW_TYPE_INTERFACE_Session, - NULL, WP_PROXY_FEATURE_INFO | WP_PROXY_SESSION_FEATURE_DEFAULT_ENDPOINT); - g_signal_connect (sessions_om, "objects-changed", - (GCallback) sessions_changed, self); - wp_core_install_object_manager (self->core, sessions_om); - - /* install object manager for Audio/Sink endpoints */ - g_variant_builder_init (&b, G_VARIANT_TYPE ("aa{sv}")); - g_variant_builder_open (&b, G_VARIANT_TYPE_VARDICT); - g_variant_builder_add (&b, "{sv}", "type", - g_variant_new_int32 (WP_OBJECT_MANAGER_CONSTRAINT_PW_GLOBAL_PROPERTY)); - g_variant_builder_add (&b, "{sv}", "name", - g_variant_new_string (PW_KEY_MEDIA_CLASS)); - g_variant_builder_add (&b, "{sv}", "value", - g_variant_new_string ("Audio/Sink")); - g_variant_builder_close (&b); - - wp_object_manager_add_proxy_interest (endpoints_om, - PW_TYPE_INTERFACE_Endpoint, - g_variant_builder_end (&b), - WP_PROXY_FEATURE_INFO | WP_PROXY_ENDPOINT_FEATURE_CONTROLS); - g_signal_connect (endpoints_om, "objects-changed", - (GCallback) endpoints_changed, self); - wp_core_install_object_manager (self->core, endpoints_om); - - g_signal_connect (self->core, "remote-state-changed", - (GCallback) remote_state_changed, self); + wp_core_disconnect (self->core); + g_clear_object (&self->default_nodes_api); + g_clear_object (&self->mixer_api); + g_object_unref (self->core); - g_main_loop_run (self->loop); g_main_context_pop_thread_default (self->context); + g_main_loop_unref (self->loop); + g_main_context_unref (self->context); return NULL; } -static void -mixer_control_impl_free (struct mixer_control_impl * impl) -{ - if (impl->endpoint) - g_signal_handlers_disconnect_matched (impl->endpoint, - G_SIGNAL_MATCH_FUNC, 0, 0, NULL, control_changed, NULL); - g_clear_object (&impl->endpoint); - g_free (impl); -} - struct audiomixer * audiomixer_new (void) { struct audiomixer *self = calloc(1, sizeof(struct audiomixer)); + wp_init (WP_INIT_ALL); + g_mutex_init (&self->lock); g_cond_init (&self->cond); - self->context = g_main_context_new (); - self->loop = g_main_loop_new (self->context, FALSE); - self->core = wp_core_new (self->context, NULL); - - self->state = WP_REMOTE_STATE_UNCONNECTED; - self->mixer_controls = g_ptr_array_new_with_free_func ( - (GDestroyNotify) mixer_control_impl_free); - g_weak_ref_init (&self->session, NULL); + self->mixer_controls = g_ptr_array_new_with_free_func (g_free); + g_mutex_lock (&self->lock); + self->initialized = 0; self->thread = g_thread_new ("audiomixer", (GThreadFunc) audiomixer_thread, self); + while (self->initialized == 0) + g_cond_wait (&self->cond, &self->lock); + g_mutex_unlock (&self->lock); return self; } @@ -333,11 +313,7 @@ audiomixer_free(struct audiomixer *self) g_main_loop_quit (self->loop); g_thread_join (self->thread); - g_weak_ref_clear (&self->session); g_ptr_array_unref (self->mixer_controls); - g_object_unref (self->core); - g_main_loop_unref (self->loop); - g_main_context_unref (self->context); g_cond_clear (&self->cond); g_mutex_clear (&self->lock); @@ -356,34 +332,24 @@ audiomixer_unlock(struct audiomixer *self) g_mutex_unlock (&self->lock); } -int -audiomixer_ensure_connected(struct audiomixer *self, int timeout_sec) +static gboolean +do_connect (WpCore * core) { - gint64 end_time = g_get_monotonic_time () + timeout_sec * G_TIME_SPAN_SECOND; - - /* already connected */ - if (self->state == WP_REMOTE_STATE_CONNECTED) - return 0; - - /* if disconnected, for any reason, try to connect again */ - if (self->state != WP_REMOTE_STATE_CONNECTING) - wp_core_idle_add (self->core, (GSourceFunc) connect_in_idle, self, NULL); - - while (true) { - if (!g_cond_wait_until (&self->cond, &self->lock, end_time)) - return -ETIMEDOUT; - - if (self->state == WP_REMOTE_STATE_CONNECTED) - return 0; - else if (self->state == WP_REMOTE_STATE_ERROR) - return -EIO; - } + if (!wp_core_connect (core)) + g_warning ("Failed to connect to PipeWire"); + return G_SOURCE_REMOVE; } int audiomixer_ensure_controls(struct audiomixer *self, int timeout_sec) { gint64 end_time = g_get_monotonic_time () + timeout_sec * G_TIME_SPAN_SECOND; + + g_return_val_if_fail (self->initialized == 1, -EIO); + + if (!wp_core_is_connected (self->core)) + g_main_context_invoke (self->context, (GSourceFunc) do_connect, self->core); + while (self->mixer_controls->len == 0) { if (!g_cond_wait_until (&self->cond, &self->lock, end_time)) return -ETIMEDOUT; @@ -425,17 +391,17 @@ audiomixer_add_event_listener(struct audiomixer *self, static gboolean do_change_volume (struct action * action) { - struct mixer_control_impl *ctl; struct audiomixer *self = action->audiomixer; + GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + gboolean ret = FALSE; + + g_variant_builder_add (&b, "{sv}", "volume", + g_variant_new_double (action->change_volume.volume)); + g_signal_emit_by_name (self->mixer_api, "set-volume", + action->change_volume.id, g_variant_builder_end (&b), &ret); + if (!ret) + g_warning ("mixer api set-volume failed"); - for (guint i = 0; i < self->mixer_controls->len; i++) { - ctl = g_ptr_array_index (self->mixer_controls, i); - if (ctl->id != action->change_volume.id) - continue; - if (ctl->is_master) - wp_endpoint_set_control_float (ctl->endpoint, - WP_ENDPOINT_CONTROL_VOLUME, action->change_volume.volume); - } return G_SOURCE_REMOVE; } @@ -448,29 +414,31 @@ audiomixer_change_volume(struct audiomixer *self, (const struct mixer_control_impl *) control; struct action * action; + g_return_if_fail (self->initialized == 1); + /* schedule the action to run on the audiomixer thread */ action = g_new0 (struct action, 1); action->audiomixer = self; - action->change_volume.id = impl->id; + action->change_volume.id = impl->node_id; action->change_volume.volume = (gfloat) volume; - wp_core_idle_add (self->core, (GSourceFunc) do_change_volume, action, + wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_volume, action, g_free); } static gboolean do_change_mute (struct action * action) { - struct mixer_control_impl *ctl; struct audiomixer *self = action->audiomixer; + GVariantBuilder b = G_VARIANT_BUILDER_INIT (G_VARIANT_TYPE_VARDICT); + gboolean ret = FALSE; + + g_variant_builder_add (&b, "{sv}", "mute", + g_variant_new_double (action->change_mute.mute)); + g_signal_emit_by_name (self->mixer_api, "set-volume", + action->change_mute.id, g_variant_builder_end (&b), &ret); + if (!ret) + g_warning ("mixer api set-volume failed"); - for (guint i = 0; i < self->mixer_controls->len; i++) { - ctl = g_ptr_array_index (self->mixer_controls, i); - if (ctl->id != action->change_mute.id) - continue; - if (ctl->is_master) - wp_endpoint_set_control_boolean (ctl->endpoint, - WP_ENDPOINT_CONTROL_MUTE, action->change_mute.mute); - } return G_SOURCE_REMOVE; } @@ -483,11 +451,13 @@ audiomixer_change_mute(struct audiomixer *self, (const struct mixer_control_impl *) control; struct action * action; + g_return_if_fail (self->initialized == 1); + /* schedule the action to run on the audiomixer thread */ action = g_new0 (struct action, 1); action->audiomixer = self; - action->change_mute.id = impl->id; + action->change_mute.id = impl->node_id; action->change_mute.mute = mute; - wp_core_idle_add (self->core, (GSourceFunc) do_change_mute, action, + wp_core_idle_add (self->core, NULL, (GSourceFunc) do_change_mute, action, g_free); } |