Ashok Sidipotu
June 15, 2023
Reading time:
This is the second blog post in the series about the upcoming release of WirePlumber 0.5. The first post discussed the WirePlumber configuration system refactoring. This post will focus on the new Event Dispatcher.
In a nutshell, the Event Dispatcher is a custom PipeWire event scheduling mechanism designed to address many of the fundamental issues in WirePlumber. The idea was conceived by my colleague and mentor George Kiagiadakis, the principal author of WirePlumber, whom I feel privileged to be working with. George not only conceived the idea but also made all the changes to the core WirePlumber library (libwireplumber) to support this mechanism. He presented the idea to us when he was close to finishing the core changes.
I immediately appreciated the value of the idea and was delighted when he asked me to port all the Lua scripts and WirePlumber modules to the new Event Dispatcher. We have been working on this for almost seven months now. During this process, I ported all the scripts and modules, and also made a few critical changes to the core Event Dispatcher. After all this work, I feel that WirePlumber has matured and is now ready to handle any real-world problems.
PipeWire maintains a collection of objects, such as devices, nodes, ports, and links, which are queried and updated via a protocol on a local Unix socket. While the protocol itself is synchronous, there are many cases where retrieving or updating information on objects requires multiple consecutive protocol calls, which means that any operation can take a non-trivial amount of time. During that time, it is possible for other PipeWire clients to also query and/or update the same objects, introducing concurrency problems from the perspective of a single client.
To address these problems, WirePlumber has been designed to hide the complexity of the protocol behind an asynchronous object API. This API is then consumed by modules and scripts to build logic that interacts with the PipeWire objects.
Unfortunately, this API has its limitations. Firstly, to be notified about events on these objects, modules and scripts register callbacks. Even though WirePlumber is single-threaded, there is no mechanism to guarantee the order of execution of these callbacks. This means that different modules that need to react to the same event may be triggered in random order. Secondly, making changes to any PipeWire object starts an asynchronous operation. Usually these changes need to be performed as a reaction to some event, but since there are multiple callbacks in different modules that react to the same event, it is possible that they all start making similar changes to the same PipeWire object without waiting for each other to finish.This may cause interference between operations, causing issues and necessitating additional handling to prevent them.
This problem is better explained with an example. Consider that a new device (e.g. a USB headset) is connected to the system. The ALSA device monitor then creates a new device object on PipeWire. This translates to a new device added signal in WirePlumber that triggers the policy-device-profile.lua script, which selects and sets the profile of the device. On the same signal, another script (policy-device-routes.lua) enables the routes of the device (i.e. sub-device paths such as speakers or headsets on the sound card). The routes, however, are dependent on the profile, so ideally the profile needs to be selected first. Otherwise, the routes script will select routes for the initial device profile and when the profile is changed by the first script, the routes will need to be re-evaluated.
When the profile is selected, the device monitor proceeds in creating one or more nodes corresponding to the individual inputs and outputs (e.g., speakers and microphone). This produces one or more new node added signals. Then, the restore-stream.lua script is triggered to check whether a node is a stream in order to restore previously stored stream properties, such as volume, mute status, channel map, and channel volume. On the same signal, the module-default-nodes.c module re-calculates the default sink and source, while the create-item.lua script creates a session item object to control this node.
Unfortunately, the device monitor will not wait for the policy-device-profile.lua script to select a profile and may create nodes for the initial device profile earlier. That means that while all the operations started by the new node added handlers are working on their logic, the nodes may actually be destroyed and re-created, requiring all of that to be executed again.
Finally, when the session item is created, a session item created signal triggers the policy-node.lua script to rescan the graph and potentially link some nodes together (e.g. stream nodes with device nodes). The logic of this script depends on the default sink and source that have been selected. However, it is not guaranteed that module-default-nodes.c will have finished its operation earlier, so checks are required to ensure that policy-node.lua does not link streams to the old default source or sink.
As you can see, there are many signal handlers listening on the same signals and starting operations that may interfere with each other. This situation leads to many race conditions and requires ugly hacks as a way out (the main policy script, policy-node.lua, is filled with them). It also leads to a lot of redundancy in code as similar checks are performed in various places.
To address this problem, we have come up with a new approach: the Event Dispatcher. This new mechanism takes all PipeWire event signals and converts them into event objects, which are then pushed into a priority queue with a pre-defined priority number assigned to each event. The events are then dispatched according to that priority, allowing for a predictable order of execution.
Unlike the previous approach of registering callbacks directly on pipewire objects, all signal handlers are now implemented as hook objects, which register themselves with the events in the queue. These hooks have dependencies between them, which ensures that the order of their execution is also predictable.
Hooks may be synchronous or asynchronous. Being synchronous means that they consist of only one function that performs a task and completes the operation immediately. Being asynchronous means that they consist of multiple functions and that they perform operations on objects that may take some time to be completed.
In either case, the event dispatcher only allows one hook to be running at any given time. For asynchronous hooks, this means that the entire operation needs to complete before another hook can be executed. This ensures that there is no interference between operations.
Event objects are ephemeral, meaning they are created in response to a PipeWire event, they are placed in the priority queue and they are destroyed after they are dispatched i.e all the registered hooks have been executed. Hook objects, on the other hand, are perennial, meaning they are always registered with the Event Dispatcher and are waiting for events.
Each hook can declare interest in specific events with the same Interest mechanism that we have been using already for Object Managers. When a hook is "interested" in an event, it means that properties declared on the Interest object match the properties of the event. When an event is pushed into the queue, hooks that are “interested” in this event are collected into a list on the event object and are sorted based on their inter-dependencies. When the event is dispatched, its collected hooks are executed one by one, in the order they appear in that list.
The Event Dispatcher also features a preemption scheme that enables higher priority events to interrupt and take precedence over lower priority events that are currently being dispatched. During the event dispatch process, once a hook finishes executing for a particular event, the Event Dispatcher checks if there are any higher priority events waiting in the queue. If there is one, the Event Dispatcher switches the current event being dispatched and starts executing the hooks associated with the higher priority event.
This means that the hooks corresponding to the newer, higher priority event are executed before the remaining hooks for the current event. This ensures that events are processed in the order of their priority, with higher priority events being handled as soon as possible.
When George first presented the idea, it took a while to sink in for us. It needed some reflection. To help you to this end, the brief video below demonstrates how the Event Dispatcher works in a common scenario: Bluetooth auto-switching.
In this scenario, a Bluetooth headset is already connected and set to the A2DP profile, which features high quality audio but with the microphone being disabled. Then, the user starts a Zoom call, which requires audio input. WirePlumber then automatically switches the headset to the HFP profile, which allows the microphone to work.
The Event Dispatcher not only has solved the main problem we were facing, but it has also fundamentally changed the way we approach WirePlumber's Lua scripts. We now see each script as a hook that responds to an event, playing a small part in a series of operations that are performed by different hooks that respond to the same event. That allows us to imagine WirePlumber as a collection of hooks that respond to various events and influence decisions and operations to a small extent.
We have revisited and reworked all the principal tasks and scripts, such as restore-stream, default-nodes, policy-node, etc, taking them through the lens of the Event Dispatcher. In this process, we had the opportunity to clean them up and remove a lot of hacks, as well as break them up into smaller, more manageable pieces. The result is Lua code that is modular, user-configurable, and easy to extend. In my next blog post, I will likely discuss the topic of policy cleanup in more detail.
If you liked what you read so far, I would like to take few more minutes of your time to present a few more implementation details.
Here is a hook that takes the default sink or source, which is selected by previous hooks, and applies it by updating the default PipeWire metadata. Today, this task is performed by a WirePlumber module, module-default-nodes.c, which is not very modular.
SimpleEventHook { name = "default-nodes/apply-default-node", after = { "default-nodes/find-best-default-node", "default-nodes/find-echo-cancel-default-node", "default-nodes/find-selected-default-node", "default-nodes/find-stored-default-node" }, interests = { EventInterest { Constraint { "event.type", "=", "select-default-node" }, }, }, execute = function (event) local source = event:get_source () local props = event:get_properties () local def_node_type = props ["default-node.type"] local selected_node = event:get_data ("selected-node") local om = source:call ("get-object-manager", "metadata") local metadata = om:lookup { Constraint { "metadata.name", "=", "default" } } if selected_node then local key = "default." .. def_node_type Log.info ("set default node for " .. key .. " " .. selected_node) metadata:set (0, key, "Spa:String:JSON", Json.Object { ["name"] = selected_node }:to_string ()) else metadata:set (0, "default." .. def_node_type, nil, nil) end end }:register ()
Here is the anatomy of a hook with out too many gory details.
name
.after
/before
tags, inspired by systemd, which can list the names of other hooks that must be executed before or after this hook, respectively.interests
. EventInterest
tables use the same WpObjectInterest
API that is also used in ObjectManager
today. Constraints are not just limited to the event type, but can also list properties of the object that caused this event, allowing more complex filtering.event
. This is for simple (synchronous) hooks. Asynchronous hooks have a state machine with multiple functions as their body.event
includes references to the “subject” (the object that triggered this event), its properties, additional event-specific data as well as the “source”, which is the object that converts all the signals to event objects and maintains object managers that are shared between all hooks.register
ed with the Event Dispatcher.Looking at this hook now, imagine you want to influence the logic of selecting a default sink. Instead of editing existing scripts, you can write your logic as a hook in your own Lua source file. Add a before = "default-nodes/apply-default-node"
tag and an after
tag that lists all the other upstream hooks (as shown above) and that’s it! Your hook will now execute just before the hook shown above, after all the other selection logic has already been executed. In the body function you now have a chance to change the selected default sink before it is applied on the metadata, without changing a single line of code upstream. Today all this logic sits in a monolithic module (module-default-nodes.c) and doing such an intervention requires you to understand all of it and change it.
Almost all the needed changes have landed in the next branch. The branch is in reasonably good shape, as myself and a few colleagues have been using it without any issues for quite some time.
If you have come this far, I kindly ask you that you extend the favor by trying this branch out and let us know what you think. If you have any suggestions or encounter any issues, please do not hesitate to inform us by filing a ticket on the issue tracker
Later this year, this branch will be rolled out as WirePlumber 0.5. This will be a major upgrade that will also include other fundamental changes, such as the configuration system refactoring. We are aiming to do a few more blog posts on this release, so if you are equally enthusiastic about learning more, stay tuned!
08/10/2024
Having multiple developers work on pre-merge testing distributes the process and ensures that every contribution is rigorously tested before…
15/08/2024
After rigorous debugging, a new unit testing framework was added to the backend compiler for NVK. This is a walkthrough of the steps taken…
01/08/2024
We're reflecting on the steps taken as we continually seek to improve Linux kernel integration. This will include more detail about the…
27/06/2024
With each board running a mainline-first Linux software stack and tested in a CI loop with the LAVA test framework, the Farm showcased Collabora's…
26/06/2024
WirePlumber 0.5 arrived recently with many new and essential features including the Smart Filter Policy, enabling audio filters to automatically…
12/06/2024
Part 3 of the cmtp-responder series with a focus on USB gadgets explores several new elements including a unified build environment with…
Comments (5)
Daniel:
Jul 18, 2023 at 08:41 AM
This looks amazing! Thank you for your great work, Ashok and team. Is there a specific date on when 0.5 will become an official release?
Reply to this comment
Reply to this comment
Ashok Sidipotu:
Jul 18, 2023 at 02:30 PM
Thank you Daniel, appreciate your feedback. No concrete date yet, It is very likely to happen some time this year.
Reply to this comment
Reply to this comment
pallaswept:
Aug 03, 2023 at 03:20 PM
TL;DR Thanks! there's a Tumbleweed package over here: https://build.opensuse.org/package/show/home:pallaswept/wireplumber
Ashok, thanks very much for your blog posts keeping us in the loop about things to come, and to you and the entire team for the great work you're doing with linux audio.
While I found pipewire+wireplumber's default behaviour to be very sane for the normal user, I have very unusual requirements of my audio environment, so I am about to embark on making a LOT of changes to pipewire and especially wireplumber's configuration.
So instead of doing a few weeks' work making wireplumber fit my unusual requirements, only to have to do it all again in a matter of months when wp 0.5.x is officially released, I wanted to just start with 0.5 now. So, since you've so kindly invited us to try it out, I will do just that. I very much appreciate your blog posts saving me from doing a very big job twice, thank you!!
But, I am very averse to installing packages outside of control of my package manager. so I took OpenSuSE's existing wp 0.4.14 build and made a few changes to have it build 0.5. I know it's really still called 0.4.80, but this package will just use the 'next' branch of wp as it's source, and call itself version 0.5.
This package has worked for me out of the box (once I remembered to remove my existing config files from ~/.config - they didn't play nicely with the new API) so I thought I'd put it in the public OBS repository, so that other Tumbleweed users who would like to try out the 'next' wireplumber can do so with ease and under complete control of their package manager. If things go south, they can easily coll back to
My apologies to any OpenSuSE Leap users, the build failed on OBS and without a Leap box of my own to try to fix it, it's only working for TW for now. Please feel free to improve upon the package, I'm new to OpenSuSE packaging so I will take no offence from being 'corrected' ;)
I hope the package is helpful to somebody out there, and thanks again Ashok for taking the time to write these blog posts and save my time!
Reply to this comment
Reply to this comment
pallaswept:
Aug 04, 2023 at 01:09 PM
I've made an OBS build of the 'next' branch, which is really 0.4.80, but mine calls itself 0.5.0, so that OpenSUSE Tumbleweed and Leap 15.5 users can get ready for the upcoming change, and since it's a different version number with different package names, you can easily switch back to the official 0.4.x if you don't like it. It's over here: https://build.opensuse.org/package/show/home%3Apallaswept/wireplumber and I'm using it right now. Thanks for keeping us in the loop, Ashok!
Reply to this comment
Reply to this comment
Ashok:
Aug 17, 2023 at 02:23 AM
thanks pallaswept!! glad that wireplumber is serving your purpose.
Reply to this comment
Reply to this comment
Add a Comment