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", diff --git a/data/media-role-nodes.conf b/data/media-role-nodes.conf new file mode 100644 index 0000000..fd37d20 --- /dev/null +++ b/data/media-role-nodes.conf @@ -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 + } +] 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/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 + } +] 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') 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 diff --git a/src/fbd-dev-sound.c b/src/fbd-dev-sound.c index 3e11c0d..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" @@ -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"; 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)