From 7fb23cfea092dfed722274801be666fc988a5dcf Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Mon, 31 Mar 2025 11:14:33 -0400 Subject: [PATCH 1/2] modules: Add login1-manager module This allows handling the prepare-for-sleep signal from the login1 Manager D-Bus interface to know when the system is suspended and resumed. --- modules/meson.build | 10 ++ modules/module-login1-manager.c | 255 ++++++++++++++++++++++++++++++++ src/config/wireplumber.conf | 7 + 3 files changed, 272 insertions(+) create mode 100644 modules/module-login1-manager.c diff --git a/modules/meson.build b/modules/meson.build index 7308aa4e..03b9a9b9 100644 --- a/modules/meson.build +++ b/modules/meson.build @@ -68,6 +68,16 @@ shared_library( dependencies : [wp_dep, giounix_dep], ) +shared_library( + 'wireplumber-module-login1-manager', + [ + 'module-login1-manager.c', + ], + install : true, + install_dir : wireplumber_module_dir, + dependencies : [wp_dep], +) + shared_library( 'wireplumber-module-si-audio-adapter', [ diff --git a/modules/module-login1-manager.c b/modules/module-login1-manager.c new file mode 100644 index 00000000..fc459c08 --- /dev/null +++ b/modules/module-login1-manager.c @@ -0,0 +1,255 @@ +/* WirePlumber + * + * Copyright © 2025 Collabora Ltd. + * @author Julian Bouzas + * + * SPDX-License-Identifier: MIT + */ + +#include +#include "dbus-connection-state.h" + +#define LOGIND_BUS_NAME "org.freedesktop.login1" +#define LOGIND_IFACE_NAME "org.freedesktop.login1.Manager" +#define LOGIND_OBJ_PATH "/org/freedesktop/login1" + +WP_DEFINE_LOCAL_LOG_TOPIC ("m-login1-manager") + +enum +{ + ACTION_GET_DBUS, + ACTION_INHIBIT, + ACTION_CLOSE, + SIGNAL_PREPARE_FOR_SLEEP, + LAST_SIGNAL +}; + +static guint signals[LAST_SIGNAL] = { 0 }; + +struct _WpLogin1ManagerPlugin +{ + WpPlugin parent; + + WpPlugin *dbus; + guint signal_id; +}; + +G_DECLARE_FINAL_TYPE (WpLogin1ManagerPlugin, wp_login1_manager_plugin, WP, + LOGIN1_MANAGER_PLUGIN, WpPlugin) +G_DEFINE_TYPE (WpLogin1ManagerPlugin, wp_login1_manager_plugin, + WP_TYPE_PLUGIN) + +static gpointer +wp_login1_manager_plugin_get_dbus (WpLogin1ManagerPlugin *self) +{ + return self->dbus ? g_object_ref (self->dbus) : NULL; +} + +static gint +wp_login1_manager_plugin_inhibit (WpLogin1ManagerPlugin *self, + const gchar *what, const gchar *who, const gchar *why, const gchar *mode) +{ + g_autoptr (GDBusConnection) conn = NULL; + g_autoptr (GError) error = NULL; + g_autoptr (GVariant) res = NULL; + gint fd; + + g_object_get (self->dbus, "connection", &conn, NULL); + g_return_val_if_fail (conn, -1); + + /* Inhibit */ + res = g_dbus_connection_call_sync (conn, LOGIND_BUS_NAME, + LOGIND_OBJ_PATH, LOGIND_IFACE_NAME, "Inhibit", + g_variant_new ("(ssss)", what, who, why, mode), G_VARIANT_TYPE ("(h)"), + G_DBUS_CALL_FLAGS_NONE, -1, NULL, &error); + if (error) { + g_autofree gchar *remote_error = g_dbus_error_get_remote_error (error); + g_dbus_error_strip_remote_error (error); + + wp_warning_object (self, "Inhibit: %s (%s)", error->message, remote_error); + return -1; + } + + /* Extract the file descriptor and return it */ + g_variant_get (res, "(h)", &fd); + return fd; +} + +static void +wp_login1_manager_plugin_close (WpLogin1ManagerPlugin *self, const gint fd) +{ + close (fd); +} + +static void +wp_login1_manager_plugin_prepare_for_sleep (GDBusConnection *connection, + const gchar *sender_name, const gchar *object_path, + const gchar *interface_name, const gchar *signal_name, + GVariant *parameters, gpointer user_data) +{ + WpLogin1ManagerPlugin *self = WP_LOGIN1_MANAGER_PLUGIN (user_data); + gboolean start = FALSE; + + g_return_if_fail (parameters); + g_variant_get (parameters, "(b)", &start); + + g_signal_emit (self, signals[SIGNAL_PREPARE_FOR_SLEEP], 0, start); +} + +static void +clear_signal (WpLogin1ManagerPlugin *self) +{ + g_autoptr (GDBusConnection) conn = NULL; + + g_object_get (self->dbus, "connection", &conn, NULL); + if (conn && self->signal_id > 0) { + g_dbus_connection_signal_unsubscribe (conn, self->signal_id); + self->signal_id = 0; + } +} + +static void +on_dbus_state_changed (GObject * obj, GParamSpec * spec, + WpLogin1ManagerPlugin *self) +{ + WpDBusConnectionState state = -1; + g_object_get (self->dbus, "state", &state, NULL); + + switch (state) { + case WP_DBUS_CONNECTION_STATE_CONNECTED: { + g_autoptr (GDBusConnection) conn = NULL; + + g_object_get (self->dbus, "connection", &conn, NULL); + g_return_if_fail (conn); + + self->signal_id = g_dbus_connection_signal_subscribe (conn, + LOGIND_BUS_NAME, LOGIND_IFACE_NAME, "PrepareForSleep", + LOGIND_OBJ_PATH, NULL, G_DBUS_SIGNAL_FLAGS_NONE, + wp_login1_manager_plugin_prepare_for_sleep, self, NULL); + break; + } + + case WP_DBUS_CONNECTION_STATE_CONNECTING: + case WP_DBUS_CONNECTION_STATE_CLOSED: + clear_signal (self); + break; + + default: + g_assert_not_reached (); + } +} + +static void +wp_login1_manager_plugin_init (WpLogin1ManagerPlugin * self) +{ +} + +static void +wp_login1_manager_plugin_enable (WpPlugin * plugin, WpTransition * transition) +{ + WpLogin1ManagerPlugin *self = WP_LOGIN1_MANAGER_PLUGIN (plugin); + g_autoptr (WpCore) core = wp_object_get_core (WP_OBJECT (self)); + + self->dbus = wp_plugin_find (core, "system-dbus-connection"); + if (!self->dbus) { + wp_transition_return_error (transition, g_error_new (WP_DOMAIN_LIBRARY, + WP_LIBRARY_ERROR_INVARIANT, + "system-dbus-connection module must be loaded before login1-manager")); + return; + } + + g_signal_connect_object (self->dbus, "notify::state", + G_CALLBACK (on_dbus_state_changed), self, 0); + on_dbus_state_changed (G_OBJECT (self->dbus), NULL, self); + + wp_object_update_features (WP_OBJECT (self), WP_PLUGIN_FEATURE_ENABLED, 0); +} + +static void +wp_login1_manager_plugin_disable (WpPlugin * plugin) +{ + WpLogin1ManagerPlugin *self = WP_LOGIN1_MANAGER_PLUGIN (plugin); + + clear_signal (self); + g_clear_object (&self->dbus); + + wp_object_update_features (WP_OBJECT (self), 0, WP_PLUGIN_FEATURE_ENABLED); +} + +static void +wp_login1_manager_plugin_class_init ( + WpLogin1ManagerPluginClass * klass) +{ + WpPluginClass *plugin_class = (WpPluginClass *) klass; + + plugin_class->enable = wp_login1_manager_plugin_enable; + plugin_class->disable = wp_login1_manager_plugin_disable; + + /** + * WpLogin1ManagerPlugin::get-dbus: + * + * Returns: (transfer full): the dbus object + */ + signals[ACTION_GET_DBUS] = g_signal_new_class_handler ( + "get-dbus", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_login1_manager_plugin_get_dbus, + NULL, NULL, NULL, + G_TYPE_OBJECT, 0); + + /** + * WpLogin1ManagerPlugin::inhibit: + * + * @brief + * @em what: what type to inhibit + * @em who: who will inhibit + * @em why: reason to inhibit + * @em mode: the inhibit mode + * + * Inhibits system shutdowns and sleep states + */ + signals[ACTION_INHIBIT] = g_signal_new_class_handler ( + "inhibit", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_login1_manager_plugin_inhibit, + NULL, NULL, NULL, G_TYPE_INT, + 4, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING, G_TYPE_STRING); + + /** + * WpLogin1ManagerPlugin::close: + * + * @brief + * @em fd: the file descriptor + * + * Closes the file descriptor returned by Inhibit to release inhibition + */ + signals[ACTION_CLOSE] = g_signal_new_class_handler ( + "close", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST | G_SIGNAL_ACTION, + (GCallback) wp_login1_manager_plugin_close, + NULL, NULL, NULL, G_TYPE_NONE, + 1, G_TYPE_INT); + + + /** + * WpLogin1ManagerPlugin::changed: + * + * @brief + * @em start: TRUE if going to sleep, FALSE if resuming + * + * Signaled when system suspends or resums + */ + signals[SIGNAL_PREPARE_FOR_SLEEP] = g_signal_new ( + "prepare-for-sleep", G_TYPE_FROM_CLASS (klass), + G_SIGNAL_RUN_LAST, 0, + NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_BOOLEAN); +} + +WP_PLUGIN_EXPORT GObject * +wireplumber__module_init (WpCore * core, GVariant * args, GError ** error) +{ + return G_OBJECT (g_object_new (wp_login1_manager_plugin_get_type(), + "name", "login1-manager", + "core", core, + NULL)); +} diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index f3b70477..81e6a7f4 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -283,6 +283,13 @@ wireplumber.components = [ requires = [ support.dbus ] } + ## Module managing the login1 D-Bus interface + { + name = libwireplumber-module-login1-manager, type = module + provides = support.login1-manager + requires = [ support.system-dbus ] + } + ## Needed for device reservation to work { name = libwireplumber-module-reserve-device, type = module From 021eb28a642dbd23790893d7a455c65756598d8a Mon Sep 17 00:00:00 2001 From: Julian Bouzas Date: Fri, 28 Nov 2025 10:31:11 -0500 Subject: [PATCH 2/2] scripts: add new mute-node.lua script This script auto mutes all ALSA sink nodes before suspending for 1 second. This can be useful to make sure the audio ring buffer is cleared before suspending the system. --- src/config/wireplumber.conf | 6 ++ src/scripts/node/mute-node.lua | 108 +++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/scripts/node/mute-node.lua diff --git a/src/config/wireplumber.conf b/src/config/wireplumber.conf index 81e6a7f4..592e99ce 100644 --- a/src/config/wireplumber.conf +++ b/src/config/wireplumber.conf @@ -626,6 +626,11 @@ wireplumber.components = [ name = node/suspend-node.lua, type = script/lua provides = hooks.node.suspend } + { + name = node/mute-node.lua, type = script/lua + provides = hooks.node.mute + requires = [ support.login1-manager ] + } { name = node/state-stream.lua, type = script/lua provides = hooks.stream.state @@ -642,6 +647,7 @@ wireplumber.components = [ type = virtual, provides = policy.node requires = [ hooks.node.create-session-item ] wants = [ hooks.node.suspend + hooks.node.mute hooks.stream.state hooks.filter.forward-format hooks.filter.graph ] diff --git a/src/scripts/node/mute-node.lua b/src/scripts/node/mute-node.lua new file mode 100644 index 00000000..8dd66f14 --- /dev/null +++ b/src/scripts/node/mute-node.lua @@ -0,0 +1,108 @@ +-- WirePlumber +-- +-- Copyright © 2025 Collabora Ltd. +-- @author Julian Bouzas +-- +-- SPDX-License-Identifier: MIT + +cutils = require ("common-utils") +log = Log.open_topic ("s-mute-node") + +local SUSPEND_WAIT_TIMEOUT_MSEC = 1000 +local inhibit_fd = -1 +local muted_nodes = {} +local suspend_wait_timeout_source = nil + +function inhibitBlockSleep (login1_manager, reason) + if inhibit_fd == -1 then + inhibit_fd = login1_manager:call ("inhibit", "sleep", "wireplumber", reason, "block") + if inhibit_fd == -1 then + log:info ("Could not call inhibit block sleep") + else + log:info ("Block sleep inhibit called successfully: fd=" .. tostring (inhibit_fd)) + end + else + log:info ("Block sleep inhibit is already called") + end +end + +function closeBlockSleep (login1_manager) + if inhibit_fd ~= -1 then + login1_manager:call ("close", inhibit_fd) + log:info ("Inhibit block sleep closed successfully: fd=" .. tostring (inhibit_fd)) + inhibit_fd = -1 + else + log:info ("Inhibit block sleep is already closed") + end +end + +function isNodeMuted (node) + for p in node:iterate_params ("Props") do + local props = cutils.parseParam (p, "Props") + if props ~= nil and props.mute then + return true + end + end + return false +end + +function muteNode (node, mute_value) + local pod = Pod.Object { + "Spa:Pod:Object:Param:Props", "Props", + mute = Pod.Boolean (mute_value) + } + node:set_params ("Props", pod) +end + +login1_manager = Plugin.find("login1-manager") +if login1_manager then + + -- Call inhibit block sleep + inhibitBlockSleep (login1_manager, "Audio sinks mute") + + -- handle prepare-for-sleep signal + login1_manager:connect ("prepare-for-sleep", function (p, start) + source = source or Plugin.find ("standard-event-source") + + if start then + log:info ("Suspending...") + + -- Mute all ALSA sink nodes that are not already muted + local node_om = source:call ("get-object-manager", "node") + for node in node_om:iterate() do + if node.properties ["media.class"] == "Audio/Sink" and not isNodeMuted (node) then + log:info ("Muting ALSA sink node: " .. tostring (node.properties ["node.name"])) + muteNode (node, true) + muted_nodes [node.id] = true + end + end + + -- Close block sleep after 1s to make sure the ring buffer has silent audio + log:info ("Waiting for a bit before closing inhibit block sleep...") + if suspend_wait_timeout_source ~= nil then + suspend_wait_timeout_source:destroy () + suspend_wait_timeout_source = nil + end + suspend_wait_timeout_source = Core.timeout_add (SUSPEND_WAIT_TIMEOUT_MSEC, function () + suspend_wait_timeout_source = nil + closeBlockSleep (login1_manager) + end) + else + log:info ("Resuming...") + + -- Unmute all ALSA sink nodes + local node_om = source:call ("get-object-manager", "node") + for node in node_om:iterate() do + if node.properties ["media.class"] == "Audio/Sink" and muted_nodes[node.id] then + log:info ("Unmuting ALSA sink node: " .. tostring (node.properties ["node.name"])) + muteNode (node, false) + muted_nodes [node.id] = nil + end + end + + -- Call inhibit block sleep + inhibitBlockSleep (login1_manager, "Audio sinks mute") + end + end) +end +