From 283f2ee1dc3278589056735542c85b1f36d71710 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Fri, 24 Oct 2025 13:43:00 +0200 Subject: [PATCH 1/7] run: Simplify running MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We usually want the daemon so default to that when nothing else is passed in. Signed-off-by: Guido Günther --- run.in | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/run.in b/run.in index 33b3933..9d62ba6 100755 --- a/run.in +++ b/run.in @@ -1,4 +1,4 @@ -#!/bin/sh +#!/bin/bash set -e ABS_BUILDDIR='@ABS_BUILDDIR@' @@ -14,5 +14,7 @@ if [ "${FBD_GDB}" = 1 ]; then WRAPPER="gdb --args" fi +EXEC=${@:-${ABS_BUILDDIR}/src/feedbackd} + set -x -exec ${WRAPPER} "$@" +exec ${WRAPPER} $EXEC From 5de4519710f807ee34b2f53423dba1d8abdc759a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Wed, 23 Apr 2025 18:16:40 +0200 Subject: [PATCH 2/7] data: Set suitable media roles for events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mark alarms, alerts and ring tones. Signed-off-by: Guido Günther --- data/default.json | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/data/default.json b/data/default.json index ffc51e1..604a4a2 100644 --- a/data/default.json +++ b/data/default.json @@ -7,7 +7,8 @@ { "event-name" : "alarm-clock-elapsed", "type" : "Sound", - "effect" : "alarm-clock-elapsed" + "effect" : "alarm-clock-elapsed", + "media-role" : "Alarm" }, { "event-name" : "button-pressed", @@ -42,7 +43,8 @@ { "event-name" : "message-new-cellbroadcast", "type" : "Sound", - "effect" : "cellbroadcast" + "effect" : "cellbroadcast", + "media-role" : "Emergency" }, { "event-name" : "message-new-email", @@ -62,7 +64,8 @@ { "event-name" : "phone-incoming-call", "type" : "Sound", - "effect" : "phone-incoming-call" + "effect" : "phone-incoming-call", + "media-role" : "Ringtone" }, { "event-name" : "phone-hangup", From d58b7cf517d018bbd73644cda23e0eca580c0462 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Wed, 23 Apr 2025 18:07:45 +0200 Subject: [PATCH 3/7] data: Add media roles configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This ensures we properly duck any "multimedia" streams when playing event sounds. We also introduce an alert group for e.g. cell broadcasts. For testing copy to ~/.config/wireplumber/wireplumber.conf.d/media-role-nodes.conf and systemctl --user restart wireplumber Signed-off-by: Guido Günther --- data/media-role-nodes.conf | 138 +++++++++++++++++++++++++++++++++ examples/media-role-nodes.conf | 138 +++++++++++++++++++++++++++++++++ 2 files changed, 276 insertions(+) create mode 100644 data/media-role-nodes.conf create mode 100644 examples/media-role-nodes.conf diff --git a/data/media-role-nodes.conf b/data/media-role-nodes.conf new file mode 100644 index 0000000..f25fbf5 --- /dev/null +++ b/data/media-role-nodes.conf @@ -0,0 +1,138 @@ +# Based on https://gitlab.freedesktop.org/pipewire/wireplumber/-/blob/master/src/config/wireplumber.conf.d.examples/media-role-nodes.conf + +wireplumber.profiles = { + main = { + policy.linking.role-based.loopbacks = required + } +} + +wireplumber.settings = { + # Default role + node.stream.default-media-role = "Multimedia" + linking.role-based.duck-level = 0.5 +} + +wireplumber.components.rules = [ + { + matches = [ + { + provides = "~loopback.sink.*" + } + ] + actions = { + merge = { + arguments = { + capture.props = { + # Explicitly mark all these sinks as valid role-based policy + # targets, meaning that any links between streams and these sinks + # will be managed by the role-based policy + policy.role-based.target = true + + audio.position = [ FL, FR ] + media.class = Audio/Sink + } + playback.props = { + # This must be set to ensure that the real audio sink is suspended + # when there is no active client stream linked + node.passive = true + # Set this to an unused role to make sure that loopbacks don't + # accidentally chain-link on to one another, especially when + # node.stream.default-media-role is configured in the settings + media.role = "Loopback" + } + } + requires = [ support.export-core, pw.node-factory.adapter ] + } + } + } +] + +wireplumber.components = [ + { + type = virtual, provides = policy.linking.role-based.loopbacks + requires = [ loopback.sink.role.multimedia + loopback.sink.role.notification + loopback.sink.role.alarm + loopback.sink.role.ringtone + loopback.sink.role.alert ] + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + node.name = "loopback.sink.role.multimedia" + node.description = "Multimedia" + capture.props = { + device.intended-roles = [ "Music", "Movie", "Game", "Multimedia" ] + device.icon-name = "applications-multimedia" + policy.role-based.priority = 10 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "mix" + } + } + provides = loopback.sink.role.multimedia + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # System events from e.g. notifications + node.name = "loopback.sink.role.notification" + node.description = "System Events and Notifications" + capture.props = { + device.intended-roles = [ "Notification" ] + device.icon-name = "preferences-system-notifications" + policy.role-based.priority = 20 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "duck" + } + } + provides = loopback.sink.role.notification + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Alarm clocks + node.name = "loopback.sink.role.alarm" + node.description = "Alarm Clocks" + capture.props = { + device.intended-roles = [ "Alarm" ] + device.icon-name = "alarm" + policy.role-based.priority = 30 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "duck" + } + } + provides = loopback.sink.role.alarm + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Phone call ring tones + node.name = "loopback.sink.role.ringtone" + node.description = "Ringtones" + capture.props = { + device.intended-roles = [ "Ringtone" ] + device.icon-name = "call-ring" + policy.role-based.priority = 40 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "cork" + } + } + provides = loopback.sink.role.ringtone + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Cell Broadcasts, etc + node.name = "loopback.sink.role.alert" + node.description = "Alerts" + capture.props = { + device.intended-roles = [ "Alert", "Emergency" ] + device.icon-name = "network-cellular" + policy.role-based.priority = 100 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "cork" + } + } + provides = loopback.sink.role.alert + } +] diff --git a/examples/media-role-nodes.conf b/examples/media-role-nodes.conf new file mode 100644 index 0000000..f25fbf5 --- /dev/null +++ b/examples/media-role-nodes.conf @@ -0,0 +1,138 @@ +# Based on https://gitlab.freedesktop.org/pipewire/wireplumber/-/blob/master/src/config/wireplumber.conf.d.examples/media-role-nodes.conf + +wireplumber.profiles = { + main = { + policy.linking.role-based.loopbacks = required + } +} + +wireplumber.settings = { + # Default role + node.stream.default-media-role = "Multimedia" + linking.role-based.duck-level = 0.5 +} + +wireplumber.components.rules = [ + { + matches = [ + { + provides = "~loopback.sink.*" + } + ] + actions = { + merge = { + arguments = { + capture.props = { + # Explicitly mark all these sinks as valid role-based policy + # targets, meaning that any links between streams and these sinks + # will be managed by the role-based policy + policy.role-based.target = true + + audio.position = [ FL, FR ] + media.class = Audio/Sink + } + playback.props = { + # This must be set to ensure that the real audio sink is suspended + # when there is no active client stream linked + node.passive = true + # Set this to an unused role to make sure that loopbacks don't + # accidentally chain-link on to one another, especially when + # node.stream.default-media-role is configured in the settings + media.role = "Loopback" + } + } + requires = [ support.export-core, pw.node-factory.adapter ] + } + } + } +] + +wireplumber.components = [ + { + type = virtual, provides = policy.linking.role-based.loopbacks + requires = [ loopback.sink.role.multimedia + loopback.sink.role.notification + loopback.sink.role.alarm + loopback.sink.role.ringtone + loopback.sink.role.alert ] + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + node.name = "loopback.sink.role.multimedia" + node.description = "Multimedia" + capture.props = { + device.intended-roles = [ "Music", "Movie", "Game", "Multimedia" ] + device.icon-name = "applications-multimedia" + policy.role-based.priority = 10 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "mix" + } + } + provides = loopback.sink.role.multimedia + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # System events from e.g. notifications + node.name = "loopback.sink.role.notification" + node.description = "System Events and Notifications" + capture.props = { + device.intended-roles = [ "Notification" ] + device.icon-name = "preferences-system-notifications" + policy.role-based.priority = 20 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "duck" + } + } + provides = loopback.sink.role.notification + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Alarm clocks + node.name = "loopback.sink.role.alarm" + node.description = "Alarm Clocks" + capture.props = { + device.intended-roles = [ "Alarm" ] + device.icon-name = "alarm" + policy.role-based.priority = 30 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "duck" + } + } + provides = loopback.sink.role.alarm + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Phone call ring tones + node.name = "loopback.sink.role.ringtone" + node.description = "Ringtones" + capture.props = { + device.intended-roles = [ "Ringtone" ] + device.icon-name = "call-ring" + policy.role-based.priority = 40 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "cork" + } + } + provides = loopback.sink.role.ringtone + } + { + name = libpipewire-module-loopback, type = pw-module + arguments = { + # Cell Broadcasts, etc + node.name = "loopback.sink.role.alert" + node.description = "Alerts" + capture.props = { + device.intended-roles = [ "Alert", "Emergency" ] + device.icon-name = "network-cellular" + policy.role-based.priority = 100 + policy.role-based.action.same-priority = "mix" + policy.role-based.action.lower-priority = "cork" + } + } + provides = loopback.sink.role.alert + } +] From 6296c4fe778fd23d9e30b077616bc153b06a8b06 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Thu, 23 Oct 2025 13:43:02 +0200 Subject: [PATCH 4/7] build: Allow to install media roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Install role based config if set. Signed-off-by: Guido Günther --- data/meson.build | 3 +++ meson.build | 2 ++ meson.options | 3 +++ 3 files changed, 8 insertions(+) diff --git a/data/meson.build b/data/meson.build index b68c5ae..de3310f 100644 --- a/data/meson.build +++ b/data/meson.build @@ -91,4 +91,7 @@ if get_option('daemon') install_dir: systemd_user_unit_dir, ) + if get_option('media-roles') + install_data('media-role-nodes.conf', install_dir: wireplumberdir) + endif endif # daemon diff --git a/meson.build b/meson.build index b14c4c2..bc2a572 100644 --- a/meson.build +++ b/meson.build @@ -42,6 +42,7 @@ if udev.found() and get_option('daemon') else udevdir = prefix / 'lib' / 'udev' / 'rules.d' endif +wireplumberdir = datadir / 'wireplumber' / 'wireplumber.conf.d' run_data = configuration_data() run_data.set('ABS_BUILDDIR', meson.current_build_dir()) @@ -124,6 +125,7 @@ summary = [ ' Introspection: @0@'.format(introspection), ' Vapi: @0@'.format(get_option('vapi')), ' Tests: @0@'.format(get_option('tests')), + ' Media Roles: @0@'.format(get_option('media-roles')), '---------------', '', ] diff --git a/meson.options b/meson.options index 9db4de7..55f4d36 100644 --- a/meson.options +++ b/meson.options @@ -19,3 +19,6 @@ option('tests', option('systemd_user_unit_dir', type: 'string', value: '', description: 'Directory for systemd user units') +option('media-roles', + type: 'boolean', value: false, + description: 'Use media roles') From 96b00b6c611b21c9d88d2e33c94a092ebc0a6f6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Thu, 23 Oct 2025 17:37:21 +0200 Subject: [PATCH 5/7] dev-sound: Fix format MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Gbp-Dch: Ignore Signed-off-by: Guido Günther --- src/fbd-dev-sound.c | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/fbd-dev-sound.c b/src/fbd-dev-sound.c index 3e11c0d..67891c5 100644 --- a/src/fbd-dev-sound.c +++ b/src/fbd-dev-sound.c @@ -199,7 +199,9 @@ on_sound_play_finished_callback (GSoundContext *ctx, gboolean -fbd_dev_sound_play (FbdDevSound *self, FbdFeedbackSound *feedback, FbdDevSoundPlayedCallback callback) +fbd_dev_sound_play (FbdDevSound *self, + FbdFeedbackSound *feedback, + FbdDevSoundPlayedCallback callback) { FbdAsyncData *data; const char *filename; From 8090ccb0a99496727d20932ce59a2093f3c0545f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Thu, 23 Oct 2025 13:53:06 +0200 Subject: [PATCH 6/7] dev-sound: Only set media role when role based routing is enabled MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This allows to already update the theme without breaking existing installations. Signed-off-by: Guido Günther --- src/fbd-dev-sound.c | 7 ++++++- src/meson.build | 5 +++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/fbd-dev-sound.c b/src/fbd-dev-sound.c index 67891c5..8e01fa6 100644 --- a/src/fbd-dev-sound.c +++ b/src/fbd-dev-sound.c @@ -1,5 +1,6 @@ /* * Copyright (C) 2020 Purism SPC + * 2025 Phosh.mobi e.V. * SPDX-License-Identifier: GPL-3.0+ * Author: Guido Günther * @@ -9,6 +10,8 @@ #define G_LOG_DOMAIN "fbd-dev-sound" +#include "fbd-config.h" + #include "fbd-dev-sound.h" #include "fbd-feedback-sound.h" @@ -205,7 +208,7 @@ fbd_dev_sound_play (FbdDevSound *self, { FbdAsyncData *data; const char *filename; - const char *role; + const char *role = NULL; g_return_val_if_fail (FBD_IS_DEV_SOUND (self), FALSE); g_return_val_if_fail (GSOUND_IS_CONTEXT (self->ctx), FALSE); @@ -215,7 +218,9 @@ fbd_dev_sound_play (FbdDevSound *self, if (!g_hash_table_insert (self->playbacks, feedback, data)) g_warning ("Feedback %p already present", feedback); +#ifdef FBD_USE_MEDIA_ROLES role = fbd_feedback_sound_get_media_role (feedback); +#endif if (!role) role = "event"; diff --git a/src/meson.build b/src/meson.build index 5483aca..8459bee 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,11 @@ # Feedback daemon if get_option('daemon') + config_h = configuration_data() + config_h.set_quoted('PACKAGE_VERSION', meson.project_version()) + config_h.set('FBD_USE_MEDIA_ROLES', get_option('media-roles')) + configure_file(output: 'fbd-config.h', configuration: config_h) + fbd_enum_headers = files('fbd-event.h', 'fbd-feedback-led.h', 'fbd-feedback-vibra.h') fbd_enum_sources = gnome.mkenums_simple('fbd-enums', sources: fbd_enum_headers) From f38eb4bb28a55dbf47b2772b1d4868524f29c3e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Guido=20G=C3=BCnther?= Date: Sat, 25 Oct 2025 14:10:46 +0200 Subject: [PATCH 7/7] data: Add preferred output targets for alarms and alerts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We usually want them to go to the speaker by default. This needs https://gitlab.freedesktop.org/pipewire/wireplumber/-/merge_requests/754 Signed-off-by: Guido Günther --- data/media-role-nodes.conf | 2 ++ 1 file changed, 2 insertions(+) diff --git a/data/media-role-nodes.conf b/data/media-role-nodes.conf index f25fbf5..fd37d20 100644 --- a/data/media-role-nodes.conf +++ b/data/media-role-nodes.conf @@ -99,6 +99,7 @@ wireplumber.components = [ policy.role-based.priority = 30 policy.role-based.action.same-priority = "mix" policy.role-based.action.lower-priority = "duck" + policy.role-based.preferred-target = "Speaker" } } provides = loopback.sink.role.alarm @@ -131,6 +132,7 @@ wireplumber.components = [ policy.role-based.priority = 100 policy.role-based.action.same-priority = "mix" policy.role-based.action.lower-priority = "cork" + policy.role-based.preferred-target = "Speaker" } } provides = loopback.sink.role.alert