refactor the role-based priority system (ex "endpoints") to unify it with the desktop policy
Background
A couple of years ago, we designed and developed endpoints as a mechanism to implement role-based sinks that can be backed either by software or hardware. The endpoint objects used to serve as an abstraction layer between the nodes and what the policy would need to use in order to direct application streams to the appropriate output path. This concept has been explained in this blog post. This design failed to take off, mostly because of its disconnection from the desktop "world" and the complexity of use cases that exist there. Working with endpoints on a desktop, while possible, has always been a recipe for trouble.
The original design was implemented in WP 0.3, but we soon ported everything to Lua and released 0.4. During this port, we realized the design was overly complex and due to its disconnection from desktop use cases we chose to simplify it and keep it as something specific to automotive & mobile use cases. But this disconnection also meant that there were issues in the endpoints that went undetected, as most developers normally tested on their desktops.
Starting with WP 0.5, we removed WpEndpoint
and stopped using the term endpoint altogether. The functionality has been retained, though, and ported to the new event dispatcher system. The ported Lua scripts refer to endpoints as "virtual-items", which is a strange term, but it gets the job done. In addition, the feature for loading these is called policy.role-priority-system
. This was all done as a temporary measure to retain the functionality and not let it block the 0.5 release.
Design
Going forward, I would like to unify this system with the desktop policy, so that it's easy to use features of both "worlds" (desktop and automotive/mobile) at the same time. Here is a proposed design.
First of all, the existing role-based priority system consists of 4 main components:
- There's a component that reads the config file and creates
si-audio-virtual
session items. These session items createsupport.null-audio-sink
nodes that act as loopbacks and offer the ability to control the volume of the stream that goes through them. Thesesi-audio-virtual
are the "virtual-item" (i.e. the "endpoint") objects. We create one for each stream role that we can have, i.e. "Music", "Notifications", etc. - The second component is responsible for choosing targets for application streams. Based on the stream's
media.role
property, the appropriate "virtual-item" is picked as a target, so that the stream is then linked to the output through its role's loopback node. - The loopback nodes need to be linked to the default output ALSA node on their output side. There is another component that monitors these
si-audio-virtual
and makes sure that they are linked to the default output, watching also for default output changes in order to move them to a new output if necessary. - Finally, there's a component that monitors the links between application streams and the "virtual-items" and when there's more than one link, it decides which link(s) should be active and which not or whether it needs to apply volume ducking to "lower priority" roles, for instance, to let something more important be more audible. Typical example in this case is when you have a Music and a Notification stream, both linked to their corresponding "virtual-items". The Notification stream is more important and therefore the Music "virtual-item" is temporarily ducked to 30% of its original volume, letting the Notification be more audible. Or another example is when an Alert stream is present, in which case all other streams need to cork; this implemented by deactivating all the other links, keeping only the Alert one active.
In the proposed design, the si-audio-virtual
can be removed entirely. Nowadays PipeWire supports loopbacks that are easy to create with libpipewire-module-loopback
and therefore we don't need to have anything custom. Therefore, the first component from the above list can be simply replaced with a configuration file fragment that loads a couple of libpipewire-module-loopback
components.
The second component from the above list can be replaced with a linking hook that honors "device.intended-roles", i.e. a hook that has the logic of intended-roles.lua
, ported to 0.5. The idea is, if a stream node has media.role=XYZ
and an Audio/Sink node has device.intended-roles=[XYZ, ...]
, then these two are linked together. To make this work, obviously, all the loopbacks created for this use case will need to have device.intended-roles
set.
The third component from the above list is not needed... The "Stream/Output/Audio" node of each loopback will be automatically linked to the default sink by the existing "desktop" linking policy, since it will NOT have the media.role
property set.
The fourth component from the above list still needs to exist. It should be adapted to monitor links that have been done between pairs of nodes that have a media.role
and device.intended-roles
respectively. Its logic should remain the same. Its configuration will need to be improved, though, so that it has no mention of "virtual-items".
With all these in place, it should be possible to also mix this policy with other desktop linking policy features:
- Smart filters should work as usual, so it should be easy to insert a DSP filter-chain in front of the ALSA device node without any change in the policy (this is currently a requirement in AGL and has been hacked into the endpoints policy in non-maintainable way).
- Application streams that have a specific
target.object
should still be directed to their target instead of being forced onto a role loopback. This is necessary to make the two linking policies work in harmony (and fix AGL SPEC-4694 ) - Application streams that have no particular
media.role
should be linked to the default sink, following the standard desktop linking policy. Perhaps we can also have a setting, though, to force a specificmedia.role
on streams that don't have one - this is going to be needed for automotive/mobile.
Finally, with this system it should be possible to also have ALSA device node provide their own device.intended-roles
property and take the place of the software loopbacks in case a hardware DSP exposes ALSA nodes for certain roles.
This diagram illustrates the proposed design:
graph LR
C1[client stream
media.role=Music]
C2[client stream
media.role=Notifications]
C3[client stream
media.role=null]
C4[client stream
media.role=Music
target.object=alsa_output.special]
C5[client stream
media.role=Alert]
subgraph L1[loopback - virtual Music sink]
LMsink[Audio/Sink
device.intended-roles=Music]
LMsource[Stream/Output/Audio]
LMsink -.- LMsource
end
subgraph L2[loopback - virtual Notifications sink]
LNsink[Audio/Sink
device.intended-roles=Notifications]
LNsource[Stream/Output/Audio]
LNsink -.- LNsource
end
subgraph DSP[filter-chain - soft DSP]
DSPsink[Audio/Sink
filter.smart=true]
DSPsource[Stream/Output/Audio]
DSPsink -.- DSPsource
end
D[ALSA node
Audio/Sink
@DEFAULT_AUDIO_SINK@]
D2[ALSA node
Audio/Sink
node.name=alsa_output.special
- not the default -]
D3[ALSA node
Audio/Sink
device.intended-roles=Alert]
C1 --> |r-b-p-s managed| LMsink
C2 --> |r-b-p-s managed| LNsink
C5 --> |r-b-p-s managed| D3
C4 --> D2
DSPsource --> D
C3 --> DSPsink
LMsource --> DSPsink
LNsource --> DSPsink
(r-b-p-s managed = managed by the role-based priority system, i.e. the fourth component mentioned above)