We're hiring!
*

From Lua to JSON: refactoring WirePlumber's configuration system

Ashok Sidipotu avatar

Ashok Sidipotu
October 27, 2022

Share this post:

Reading time:

Refactoring WirePlumber's configuration system is the first big feature I took up since I joined the PipeWire/WirePlumber team a year back. It's a year well spent in my professional life, hanging around with caring people and truly open source technology. With what I have seen in the multimedia stacks, I honestly believe PipeWire is the next generation multimedia server and WirePlumber playing the role of enhancing its utility and appeal.

Let me cut back to the subject at hand.

WirePlumber's old Lua configuration

As you might already know, WirePlumber is a heavily modular session/policy manager for PipeWire and it uses the Lua language both for scripting high-level logic and for defining its configuration. To avoid misunderstandings, I will only be debating the use of Lua as a configuration language.

Using Lua as a configuration language has some advantages as it integrates easily and readily with both Lua code and C code. As a plus, the implementation of rule/condition based settings (this is a hallmark of PipeWire that every entity in it is an object, every object has properties, and these properties can be used to apply settings) is a breeze in Lua.

However, there are some gaping disadvantages. To name a few: the settings cannot be changed at runtime as they're static settings, user overrides are possible but they are neither elegant nor intuitive, and validating the configuration with a schema is next to impossible.

For what it's worth, I have quite enjoyed using Lua as both the scripting language and the configuration system. However, it's time to say goodbye to using it as the configuration system.

A new JSON configuration system

I was all buoyed up when I came across the possibility of refactoring the whole WirePlumber configuration system. Honestly, who gets an opportunity to build things from the ground up like this these days? I don't know about you, but I haven't had many during my professional career.

After careful thinking and consideration, we have decided to use PipeWire's JSON syntax to define settings. This overcomes the disadvantages of the Lua configuration and also gives a more unified configuration approach across the whole PipeWire ecosystem.

PipeWire's JSON syntax is a variant of JSON called "SPA JSON" that is built into PipeWire. The SPA JSON parser is a very lightweight parser that mostly ignores all intermediate characters and therefore can parse a wide range of variants, including strict JSON. For instance, the following examples are all valid configuration files:

# Usual pipewire configuration variant
wireplumber.components = [
  { name = libwireplumber-module-default-nodes , type = module },
  { name = policy-device-profile.lua, type = script/lua }
]

# Actual JSON
"wireplumber.components" : [
  { "name" : "libwireplumber-module-default-nodes" , "type" : "module" }
  { "name" : "policy-device-profile.lua", "type" : "script/lua" }
]

# Even more loose syntax without separator characters
wireplumber.components  [
  { name  libwireplumber-module-default-nodes   type  module }
  { name  policy-device-profile.lua   type  script/lua }
]

Now back to new JSON configuration system. Settings are now defined under a new section, "wireplumber.settings", in the main configuration file (wireplumber.conf). This section is not defined as monolith but instead is distributed into different setting files (*.conf) under wireplumber.conf.d/. WirePlumber will glean through these files and stitch them up during startup.

Each conf file is a logical grouping of settings, modules, and scripts.

For example: below is the device.conf, which contains all the device-related configuration.

# Settings to Track/store/restore user choices about devices

wireplumber.settings = {
  # Below syntax defines key-value pair style settings.
  device.use-persistent-storage = true
  device.auto-echo-cancel = true
  device.echo-cancel-sink-name = echo-cancel-sink
  device.echo-cancel-source-name = echo-cancel-source

  # Below syntax defines a rule/condition based settings.
  device.rules = [
    {
      matches = [
          # Matches all devices
          { device.name = "~*" }
      ]
      actions = {
        update-props = {
          profile_names = "off pro-audio"
        }
      }
    }
  ]
}

# WirePlumber modules and scripts are also loaded from the config files.
wireplumber.components = [
  { name = libwireplumber-module-default-nodes , type = module }
  { name = policy-device-profile.lua, type = script/lua }
]

Just to make it easier for the users who are familiar with the Lua config, I drew up this table mapping the old Lua config files and their corresponding new JSON config files:

Old Lua config file New JSON config file
10-default-policy.lua policy.conf
40-device-defaults.lua, 50-default-access-config.lua device.conf
40-stream-defaults.lua stream.conf
20-default-access.lua access.conf
30-alsa-monitor.lua, 50-alsa-config.lua alsa.conf
30-libcamera-monitor.lua, 50-libcamera-config.lua libcamera.conf
30-v4l2-monitor.lua, 50-v4l2-config.lua v4l2.conf


As you might have noticed, in some cases, two Lua config files (in bold above) are merged into a single JSON config file. We hope this will facilitate much better modularization of functionality.

Now let's take a look at the system features & design, and client functions of this new JSON configuration system.

JSON configuration system features

Dynamic settings

During startup, WirePlumber loads all the settings from .conf files into a PipeWire metadata object called "sm-settings". Lua scripts, modules, and WirePlumber clients can use PipeWire metadata tools and API to change the settings at runtime. As you may know, one can issue these commands from the command prompt as well.

For example:

pw-metadata -n sm-settings 0 "policy.default.move" true Spa:String:JSON
pw-metadata -n sm-settings 0 "device.echo-cancel-source-name" "echo-cancel-source-bal" Spa:String:JSON

The above commands do not just change settings at runtime, but the changes are also applied live on WirePlumber, as explained in the below section.

Callbacks from settings

Lua scripts, modules, or WirePlumber clients interested in any of the settings can also subscribe for callbacks to know the changes in settings. This enables them to not only know the changes in settings, but also to apply the changes live.

Let me give an example to drive home the point here. You must be aware that WirePlumber saves the stream properties (volume, mute status etc). Now you can turn off this behavior runtime with the below command, no need for restart/reboot. Cool, isn't it?

pw-metadata -n sm-settings 0 stream.restore-props false Spa:String:JSON

I felt thrilled in enabling this feature across all the scripts and modules, as users can now experiment with different settings at runtime.

Please be informed that some of the changes to settings may not take effect or cause some undesired behavior. Not every setting is tested in this perspective. We may need your help here.

Easy user overrides

Easy user overrides is by far the most handy outcome of this whole exercise.

JSON facilitates for much better user/custom overrides on top of the default settings.

Sound too formal? Allow me to put things into perspective. Let's say a user wanted to customize the stream settings of WirePlumber. They would have to copy the stream config file (/usr/share/wireplumber/40-stream-defaults.lua), change the part they need to, place it in /etc/wireplumber/40-stream-defaults.lua, and restart WirePlumber. WirePlumber always loads this new configuration file and ignores the default configuration file. Now, what if this file changes upstream? In this case, the user will likely land into trouble the moment WirePlumber is upgraded.

Today the overrides work at the configuration file level. Easy overrides extend this all the way to the level of the individual setting. So this means users can only touch the settings they are interested in. We hope this will make the job of distribution packagers easier as well.

PipeWire integration

WirePlumber settings will follow the same syntax as the rest of the PipeWire and WirePlumber configs. In other words, WirePlumber settings are like any other PipeWire configuration.

Persistency

If a User/Client wants to change the settings at runtime (using pw-metadata as explained in Dynamic settings) then we recommend considering enabling persistent behavior (or simply persistency), so that the setting changes are saved to state file and are remembered across reboots.

When Persistency is enabled, the settings will be read from the config files only once and for subsequent reboots, they will be initialized from the state file. Please note that Persistency is disabled by default. It can be enabled with the below setting in wireplumber.conf

wireplumber.settings = {
  persistent.settings = true
}

Simple, powerful, and effective!

Client access

Clients that are built with the WirePlumber library will now be able to transparently access the runtime settings that the WirePlumber daemon is currently running with.

To throw another possibility at you, users can now add new settings in .conf or through pw-metadata and start querying them from their scripts/modules and build logic around it. Building this sort of developer-friendly stuff is what keeps us going.

Schema validation

JSON settings allow us to do validation against a schema. This feature has been taken into consideration, but it will not be included in the first release of this new system, as more work is required to complete it.

JSON configuration system design

As you can see, compared to Lua, we had to build quite a bit of infrastructure. Personally, I have been on this for the last 2-3 months. We believe it's all worth it in terms of the rich functionality that is described above.

JSON configuration system client functions

WirePlumber clients can access settings using two methods:

  • WP Settings API

    WpSettings loads and parses the “sm-settings” metadata, which contains WirePlumber settings and rules. It provides new APIs to its clients (modules, lua scripts, etc) to access, change, and follow them.

    Below is a quick outline of APIs.

    • wp_settings_get() API to access the values of settings.
    • wp_settings_apply_rule() to apply the rule-based settings.
    • wp_settings_subscribe() to subscribe for callbacks on settings.
  • "sm-settings" Metadata interface

    Clients can also interact with settings via the familiar PipeWire metadata tools and APIs.

Either of these APIs can be used to build a GUI front-end to modify WirePlumber settings.

Status & availability

Almost all the needed changes are landed in next-rebased branch. The branch is in reasonably good shape, myself and few of my colleagues have installed and are using it without any issues.

If you have come this far, I kindly ask you to extend the favor by trying this branch out. Please give it a try and let us know if you like it, have suggestions, or face any bugs.

A note on the WirePlumber 0.5 release

Soon, WirePlumber will be upgrading from 0.4.x to 0.5. This will be a major upgrade with significant churn. We are making some fundamental changes to the WirePlumber system, with the configs revamp being one of them. We are aiming to roll out 0.5 sometime before the end of this year.

We are aiming to do a few more blog posts on this release, so if you are equally enthusiastic about learning more, stay tuned!

Continue reading: WirePlumber's Event Dispatcher: a new, simplified way of handling PipeWire events.

Comments (27)

  1. alex:
    Oct 27, 2022 at 04:42 PM

    Maybe I'm missing something, but those configuration snippets don't look like JSON.

    Reply to this comment

    Reply to this comment

    1. Mark Filion:
      Oct 27, 2022 at 05:17 PM

      Thanks for stopping by! We've updated the blog post to clarify. PipeWire's JSON syntax is a variant of JSON called "SPA JSON" that is built into PipeWire.

      Reply to this comment

      Reply to this comment

  2. Be:
    Oct 27, 2022 at 10:37 PM

    This is great! Several months ago I ran into the issue described in this post. I had copied one of the upstream Lua scripts to my user configuration directory to change one setting. After an update, some backwards incompatible change had been made which broke my old customized Lua script. I ended up having to delete my old configuration file, copy the new upstream one, and edit it again. I hope the new declarative configuration solves this issue.

    The runtime settings modification and persistence features are cool. How could a user clear any persistent settings and reset to defaults? It would be helpful to document that clearly.

    Reply to this comment

    Reply to this comment

    1. Ashok Sidipotu:
      Oct 28, 2022 at 02:11 PM

      Sorry, that you ran into these issue and I am glad that we have a solution in place for that exact problem.

      the settings can be reset to default with the below syntax.

      wireplumber.settings = {
      persistent.settings = false.
      }

      This is a introductory post, we are working on putting handy documentation in place. Stay tuned.

      Reply to this comment

      Reply to this comment

      1. Be:
        Oct 28, 2022 at 04:24 PM

        Permanently disabling persistence in a config file wasn't what I was asking. How could a user reset to the default state once without disabling persistence? Where is the persistent state stored? Could a simple `rm -r` command do it?

        Reply to this comment

        Reply to this comment

        1. George Kiagiadakis:
          Oct 31, 2022 at 03:40 PM

          Removing the appropriate state file from ~/.local/state/wireplumber/ should return to the initial state without disabling persistence. Perhaps we can add a wpctl subcommand to do this as well.

          Reply to this comment

          Reply to this comment

  3. Ragesh:
    Oct 28, 2022 at 01:47 PM

    All the examples mentioned here are focused on desktop use cases. Do you suggest using it on embedded use cases like infotainment audio.

    Reply to this comment

    Reply to this comment

    1. Ashok Sidipotu:
      Oct 28, 2022 at 02:17 PM

      Absolutely!! Pipewire/Wireplumber is designed to serve as next generation media server for both desktop and embedded use cases. The duo is are already used in projects like AGL for infotainment audio, which is exactly what you are looking for.

      Reply to this comment

      Reply to this comment

  4. Conan Kudo (ニール・ゴンパ):
    Oct 28, 2022 at 07:57 PM

    Why use JSON instead of something more human-friendly like TOML?

    Reply to this comment

    Reply to this comment

    1. George Kiagiadakis:
      Oct 31, 2022 at 04:21 PM

      We chose to use PipeWire's configuration parsing mechanism, which uses this JSON variant, so that we minimize the amount of dependencies we have. In the past, we used to have some TOML configuration, but unfortunately TOML is not very well supported in C and we had to depend on some C++ library that was also not widely available and was just an ugly dependency.

      PipeWire itself has chosen to implement this JSON variant because it is actually very fast to parse and it allows memory-mapping the configuration file and passing entire objects or arrays down as arguments to modules just by passing pointers to the appropriate sections of the mmap'ed file, without having to copy anything in memory. The semantics of TOML would not allow this and would create an additional startup overhead, having to parse everything and fill C structures before using the configuration. Additionally, PipeWire has command-line tools that allow interfacing JSON-formatted data with JSON tools like jq to create powerful shell scripts.

      However, realizing the JSON is not the most human-friendly format (even though very convenient for the reasons stated above), PipeWire's author chose to implement this JSON variant instead of enforcing strict JSON. The variant introduces some features that are more common to typical configuration files and in some ways they resemble TOML. So, instead of having to write:

      {
      "some.object": {
      "property1": "value",
      "property2": "true"
      }
      }

      you can write instead:

      some.object = {
      property1 = value
      property2 = true
      }

      ... which is more human friendly and familiar. Of course, you can still write strict JSON and the parser will have no problem parsing it.

      Reply to this comment

      Reply to this comment

  5. Michael:
    Jan 17, 2023 at 06:14 AM

    Interesting.
    Making changes at runtime probably could be handy to resolve a problem I'm currently facing.
    Allow me to ask, if it would be possible to achive following setup:

    I'm working on a multiroom audio setup using snapserver/snapclient and pipewire as audio server:
    Assuming, a host is connected to multiple bluetooth speakers and for each of the speakers an instance of snapclient is spawned.
    Within wireplumber, the snapclient instances are only distinguishable by their corresponding application.process.id property which actually represents the pid of the system process that spawned the client.
    What I want to achieve is a static setup following mapping assuming that the pid of the snapclient processes are only known duing runtime:
    Link 1: Stream of snapclient pid X Sink of BT Speaker A
    Link 2: Stream of snapclient pid Y Sink of BT Speaker B

    Would it be possible to make the session manager automatically create the desired links at runtime. How would you achieve tthat?

    Reply to this comment

    Reply to this comment

    1. Ashok Sidipotu:
      Jan 18, 2023 at 12:47 AM

      Hi Micheal, thanks for your response.

      The setup you mentioned should be possible. Do you mind creating a support ticket at https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues to take it further?

      thanks, ashok.

      Reply to this comment

      Reply to this comment

      1. michael.rambousek@t-online.de:
        Jan 18, 2023 at 04:39 PM

        Thanks for your response.
        I opened the support ticket as requested.

        https://gitlab.freedesktop.org/pipewire/wireplumber/-/issues/406

        BR
        Michael

        Reply to this comment

        Reply to this comment

  6. Vadim:
    Jan 20, 2023 at 03:30 AM

    "Let's say a user wanted to customize the stream settings of WirePlumber. They would have to copy the stream config file ... WirePlumber always loads this new configuration file and ignores the default configuration file."

    That's not a Lua problem, but a WP problem. For one, it would be trivial to load a user config on top of the default config. Either by daisy loading (user loaded after default thus overwriting values in a normal way inside Lua) or by metatable manipulation on either side (for example attaching __index to all user tables that would point to default tables for untouched&missing values). The first method can be made very easy on the user by flattening the table hierarchy and it'll look indistinguishable from an INI file while keeping backwards-compat.

    Granted I don't know how you would serialize a Lua config while preserving all manual edits. But I don't see how that will work here either, how will the settings be stored after changing through the CLI?

    I agree that JSON is hard to read or write by hand, there's a lot of clutter. But this SPA JSON is not JSON. It is friendlier but not machine-readable by anything but your library, if the need ever arises to make changes offline.

    I didn't understand what made schema validation impossible with a Lua config. Unless you have a ready library in mind. The greatest downside of Lua is the ability to make it go brrr for a long time with a "while true; do end" - declarative configs don't have this issue.

    Disclaimer: I'm not a plumberer, a friend reads your blog because it's interesting and sent me the link. I also love Lua too much and didn't see your decisions explained well here.

    PS: The commenting system discriminates against VPN users.

    Reply to this comment

    Reply to this comment

    1. George Kiagiadakis:
      Jul 08, 2023 at 11:07 AM

      Hi Vadim, replying to your comments inline:

      > For one, it would be trivial to load a user config on top of the default config

      We do implement daisy chain loading of lua config files in the 0.4 series (not very well explained in the post, admittedly). There are 2 problems with that: the first is that you need to follow a specific order of loading, because there's a script that defines the tables, another script that defines the default values, then another script that "executes" the load_module commands (that's a custom lua function). If you need to override values in the tables, then you need to place your custom script after the one that defines the default values and before the one that loads things.

      The second problem is that users are confused with lua. Overriding a value in a table is easy enough, but adding objects inside arrays (required for specifying the "match rules" in wireplumber) is not done in the way that someone would expect... You need a "table.insert(table_name, { your object })". This is not great. Also, you need to be careful to know when you are overriding single values and when you are replacing the entire table. Users sometimes try and do "table_name = { "" = "" }" and think that this is going to override this single key, keeping the rest of the keys intact. But this is not how it works in lua.

      There are probably clever ways to define a new syntax on top of lua to overcome these problems, but is it really worth it? We only need a simple declarative configuration format and JSON offers more advantages, the way I see it.

      > Granted I don't know how you would serialize a Lua config while preserving all manual edits. But I don't see how that will work here either, how will the settings be stored after changing through the CLI?

      The changed settings are kept in a separate file, then loaded back separately from the rest of the configuration and applied manually. This could have been implemented without changing the file format as well, it has nothing to do with JSON.

      > I agree that JSON is hard to read or write by hand, there's a lot of clutter. But this SPA JSON is not JSON. It is friendlier but not machine-readable by anything but your library, if the need ever arises to make changes offline.

      There's a command line tool called "spa-json-dump", shipped together with pipewire. This tool will read SPA JSON files and dump them as standard JSON, so you can pipe the config file through that and then pipe it to any JSON-compatible utility. If you need to make changes programmatically, you can just rewrite the file in standard JSON through this tool. The SPA JSON parser will happily read standard JSON as well.

      > I didn't understand what made schema validation impossible with a Lua config. Unless you have a ready library in mind. The greatest downside of Lua is the ability to make it go brrr for a long time with a "while true; do end" - declarative configs don't have this issue.

      Schema validation is not impossible with lua, but there are tools that do this already with JSON. Also, it feels easier and safer if the configuration is declarative because you can just read the files from the safety pov of a parser, as opposed to lua where you need to execute the files, construct the tables in memory and then validate them.

      Reply to this comment

      Reply to this comment

      1. Vadim:
        Jul 08, 2023 at 05:17 PM

        Forgot to reply to this:

        > If you need to override values in the tables, then you need to place your custom script after the one that defines the default values and before the one that loads things.

        This is exactly where __table metatable would've been useful. Any values that were not defined in a user's table (key is nil) are taken (as a fallback) from the table set in there. This allows for a simple daisy-chain.
        Everything else I agree with.

        Reply to this comment

        Reply to this comment

  7. Brian Schwind:
    Jul 07, 2023 at 03:28 AM

    Maybe I missed something, but how do I simply print or dump the final, actual config for all of wireplumber? Since the tool is taking config settings from many different sources, it would be nice to view the final aggregated config.

    Reply to this comment

    Reply to this comment

    1. George Kiagiadakis:
      Jul 08, 2023 at 11:09 AM

      There is no tool at the moment to do what you suggest, but we could easily implement it. I will keep a note of that.

      Reply to this comment

      Reply to this comment

    2. Ashok Sidipotu:
      Jul 11, 2023 at 01:43 AM

      Nice suggestion, thanks!!

      Reply to this comment

      Reply to this comment

  8. Michael:
    Jul 07, 2023 at 04:31 PM

    The user override sounds really interesting and useful. It is exactly what I'm trying to do. Basically I want to override apply_properties = { ["session.suspend-timeout-seconds"] = 0 as documented here: https://davejansen.com/disable-wireplumber-pipewire-suspend-on-idle-pops-delays-noise/ for the whole system or for the current user.

    This setting is in 50-alsa-config.lua. So as per this blog entry it should be in alsa.conf. But at least in debian 12 I am not seeing a wireplumber alsa.conf and additionally I also didn't find anything in the wireplumber documentation on where the override should be placed or how it should look using JSON, either systemwide or per user. It works with the old-fashioned lua method as documented in the link. Can you point to the documentation or give an example of how this would be done with JSON? This is wireplumber 0.4.13, if it matters.

    Reply to this comment

    Reply to this comment

    1. George Kiagiadakis:
      Jul 08, 2023 at 11:11 AM

      Hi Michael, this new JSON-based configuration system is part of wireplumber 0.5, which has not been released yet. This is why you cannot find evidence of these files anywhere.

      Reply to this comment

      Reply to this comment

      1. Michael:
        Oct 07, 2023 at 04:46 PM

        ... before posting I thought I did my homework and saw wireplumber JSON files in my file system and also the wireplumbers release notes seemed to confirm that my release contains this JSON framework. Guess I didn't research thoroughly enough, so thanks for the clarification

        Reply to this comment

        Reply to this comment

    2. Ashok Sidipotu:
      Jul 11, 2023 at 01:49 AM

      Hi Micheal, Pls try https://gitlab.freedesktop.org/pipewire/wireplumber/-/tree/next. As I said in the blog post this is the branch for wireplumber 0.5

      Reply to this comment

      Reply to this comment

    3. pallaswept:
      Aug 26, 2023 at 05:25 AM

      Also, in wireplumber-next, the .conf files aren't stored in /usr/share/wireplumber or ~/.config/wireplumber, like they used to be. The .lua files are there (in a 'scripts' directory), but the wireplumber .conf files are with the pipewire conf files, in /usr/share/pipewire/wireplumber.conf.d or ~/.config/pipewire/wireplumber.conf.d.

      At least, that's how it built it for me! I assume that's an intentional change :)

      Reply to this comment

      Reply to this comment

      1. George Kiagiadakis:
        Aug 28, 2023 at 06:04 PM

        This is very intentional, yes, as we are trying to leverage pipewire's configuration system instead of having our own implementation. PipeWire currently has these paths hardcoded (well, the "pipewire" part of the path, basically), so we couldn't really deviate from that, but I believe this is a good thing.

        Reply to this comment

        Reply to this comment

        1. Patricio Serrano:
          Nov 05, 2023 at 01:38 PM

          It took me a little bit to figure out that in order to override the default `wireplumber.conf` file I have to do it in `$HOME/.config/pipewire/wireplumber.conf` and for overriding/adding lua scripts (hooks or utils) it should be done in `$HOME/.config/wireplumber/scripts/linking/my-user-script.lua`

          Reply to this comment

          Reply to this comment


Add a Comment






Allowed tags: <b><i><br>Add a new comment:


Search the newsroom

Latest Blog Posts

Automatic regression handling and reporting for the Linux Kernel

14/03/2024

In continuation with our series about Kernel Integration we'll go into more detail about how regression detection, processing, and tracking…

Almost a fully open-source boot chain for Rockchip's RK3588!

21/02/2024

Now included in our Debian images & available via our GitLab, you can build a complete, working BL31 (Boot Loader stage 3.1), and replace…

What's the latest with WirePlumber?

19/02/2024

Back in 2022, after a series of issues were found in its design, I made the call to rework some of WirePlumber's fundamentals in order to…

DRM-CI: A GitLab-CI pipeline for Linux kernel testing

08/02/2024

Continuing our Kernel Integration series, we're excited to introduce DRM-CI, a groundbreaking solution that enables developers to test their…

Persian Rug, Part 4 - The limitations of proxies

23/01/2024

This is the fourth and final part in a series on persian-rug, a Rust crate for interconnected objects. We've touched on the two big limitations:…

How to share code between Vulkan and Gallium

16/01/2024

One of the key high-level challenges of building Mesa drivers these days is figuring out how to best share code between a Vulkan driver…

Open Since 2005 logo

We use cookies on this website to ensure that you get the best experience. By continuing to use this website you are consenting to the use of these cookies. To find out more please follow this link.

Collabora Ltd © 2005-2024. All rights reserved. Privacy Notice. Sitemap.