Merge branch 'pw-roles' into 'main'

Set media-roles and add example wireplumber config

See merge request agx/feedbackd!170
This commit is contained in:
Guido Günther
2025-10-25 12:58:35 +00:00
9 changed files with 310 additions and 7 deletions
+6 -3
View File
@@ -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",
+140
View File
@@ -0,0 +1,140 @@
# 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"
policy.role-based.preferred-target = "Speaker"
}
}
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"
policy.role-based.preferred-target = "Speaker"
}
}
provides = loopback.sink.role.alert
}
]
+3
View File
@@ -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
+138
View File
@@ -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
}
]
+2
View File
@@ -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')),
'---------------',
'',
]
+3
View File
@@ -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')
+4 -2
View File
@@ -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
+9 -2
View File
@@ -1,5 +1,6 @@
/*
* Copyright (C) 2020 Purism SPC
* 2025 Phosh.mobi e.V.
* SPDX-License-Identifier: GPL-3.0+
* Author: Guido Günther <agx@sigxcpu.org>
*
@@ -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"
@@ -199,11 +202,13 @@ 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;
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);
@@ -213,7 +218,9 @@ fbd_dev_sound_play (FbdDevSound *self, FbdFeedbackSound *feedback, FbdDevSoundPl
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";
+5
View File
@@ -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)