Home

Automatically Link Pipewire Nodes with Wireplumber

Evergreen 🌳 - Published October 25, 2023 - (Updated May 14, 2024)

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.

Nodes, Ports and Links

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 Scripts

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/.

Linking Ports

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

Object Managers

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:

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

Automatically Linking Nodes

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

Running the Code

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
  }
}

Previous Versions of Wireplumber

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")

Conclusion

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:


Bennett is a Software Engineer working at Foxglove. He spends most of his day playing with TypeScript and his nights programming in Rust. You can follow him on Github.
This work by Bennett Hardwick is licensed under CC BY-NC-SA 4.0Creative Commons CC logoCreative Commons BY logoCreative Commons NC logoCreative Commons SA logo.