I'm a big fan of Pipewire on the Linux desktop. I do a bunch of amateur audio recording and music production, and have long struggled with balancing Jack and Pulseaudio in a way that just works. Pipewire and Wireplumber have mostly solved that for me - but I often find myself jumping into my patchbay to manually connect nodes every time I open certain applications.
So like anyone I decided to dive into the docs and write a Wireplumber script to automatically wire those nodes up for me.
Pipewire can be thought of like a graph that routes streams of information throughout your computer. Nodes are the nodes of the graph and Links are the edges. But Nodes can only be connected to other Nodes using ports.
These ports can have a direction (in
or out
) and can even represent certain named audio channels (such as FR
, AUX1
, AUX2
).
Wireplumber is a scriptable session manager that manages all the nodes and connections in the Pipewire graph. Most of the logic inside Wireplumber is implemented as Lua plugins which means it's super easy for you to jump in and write your own scripts and policies.
Wireplumber scripts can directly manipulate the Pipewire graph, but they need to be installed as custom components using in your Wireplumber configuration file.
For user configuration it's best to place your conf files inside ~/.config/wireplumber/wireplumber.conf.d/
and scripts under ~/.config/wireplumber/scripts/
.
Now that you know where to put these Lua scripts you can go ahead and create your first Link
object and join two ports together.
The following function link_ports
will link two Port
objects together by creating and activating a Link
object.
The Link
constructor requires an object with arguments specifying between what the connection should go:
function link_ports(output_port, input_port)
local args = {
-- The node and port to connect from
["link.output.node"] = output_port.properties["node.id"],
["link.output.port"] = output_port.properties["object.id"],
-- The node and port to connect to
["link.input.node"] = input_port.properties["node.id"],
["link.input.port"] = input_port.properties["object.id"],
-- I found that not having this entry in the args would fail
-- to create the link. Setting it to nil seems to work
["object.id"] = nil,
-- I'm not completely sure what this does but it seems to
-- make the link much more reliable
["object.linger"] = true
}
link = Link("link-factory", args)
link:activate(1)
end
Sweet! Using this method you can now connect two Port
objects together. To get these ports you need to query the Pipewire graph using an ObjectManager
The ObjectManager
is a utility that lets you query the Pipewire graph and listen for changes to particular nodes using constraints.
In this example, I'm searching for all ports that have a direction of in
and have an alias that "fuzzy matches" the string "My Output:*"
ObjectManager {
Interest {
type = "port",
Constraint {
"port.alias", "matches", "My Output:*"
},
Constraint {
"port.direction", "equals", "in"
}
}
}
Check out the Wireplumber docs to see what else constraints can do:
Constraint
Interest
ObjectManager
If you want to get a better idea of what ports and fields are available to you I've written a dump-ports.lua utility in my dotfiles that might help you out. You can execute any Wireplumber script using the wpexec
command:
wpexec ./dump-ports.lua
Now that we know how to query the Pipewire graph for ports and we can connect them using out link_ports
utility - let's write a function that combines them both together.
We'll create two object managers that will listen for input and output ports based on constraints passed in by the user.
We'll then use a mapping passed in by the user to link the two ports together whenever the object manager fires an "object-added" event.
Finally we'll activate the two object managers so they start listening for events.
If all goes well the script should look something like this:
function auto_link_ports(args)
local input_constraint = args["input"]
local output_constraint = args["output"]
local connections = args["connect"]
local input_om = ObjectManager {
Interest {
type = "port"
input_constraint,
Constraint {
"port.direction", "equals", "in"
}
}
}
local output_om = ObjectManager {
Interest {
type = "port",
input_constraint,
Constraint {
"port.direction", "equals", "out"
}
}
}
function _connect()
for output_name, input_name in pairs(args.connect) do
local output = output_om:lookup {
Constraint {
"audio.channel", "equals", output_name
}
}
local input = input_om:lookup {
Constraint {
"audio.channel", "equals", input_name
}
}
link_port(output, input)
end
end
output_om:connect("object-added", _connect)
input_om:connect("object-added", _connect)
output_om:activate()
input_om:activate()
end
That's it! Now that we've got these methods we can use it to wire up two Pipewire nodes. In this example I'm wiring up "My Output" to "My Input" and mapping ports "FL" to "AUX0" and "FR" to "AUX1":
-- ~/.config/wireplumber/scripts/my-script.lua
auto_connect_ports {
output = Constraint { "port.alias", "matches", "My Output:*"},
input = Constraint { "port.alias", "matches", "My Input:*"}
connect = {
["FL"] = "AUX0",
["FR"] = "AUX1"
}
}
Plus make sure you execute the script when Wireplumber starts by adding it to your configuration:
# ~/.config/wireplumber/wireplumber.conf.d/91-user-scripts.conf
wireplumber.components = [
{
name = ~/.config/wireplumber/scripts/auto-connect-ports.lua,
type = script/lua
provides = custom.my-script
}
]
wireplumber.profiles = {
main = {
custom.my-script = required
}
}
If you're using an older version of Wireplumber you may need to create a custom script to load the script instead of using the configuration file:
-- ~/.config/wireplumber/main.lua.d/91-user-scripts.lua
-- Start script when Wireplumber loads
load_script("~/.config/wireplumber/scripts/my-script.lua")
I'm honestly surprised that a solution to this problem hasn't been documented somewhere. I guess it's niche enough since Pipewire and Wireplumber mostly works out of the box. Still it would be nice to have some inbuilt functionality that is more advanced and can handle replacing replacing links and such. Perhaps that can be a version two.
If you want to check out this technique being used in the wild - you can find the scripts in my dotfiles:
I'm currently using these scripts to auto connect a fake stereo sink to Jack when it starts as well as to the first two channels of my multichannel audio interface.
I learned a lot from the following resources to get this script working: