← all posts

PipeWire how-to: uncomplicated loopbacks

Note: this post is now out of date. In WirePlumber 0.5 (released 2024-03-18), the config file format changed dramatically:

For example, if you had this (main.lua.d/foo.lua):

load_script("/path/to/my-script.lua")

You could now try this instead (wireplumber.conf.d/foo.conf):

wireplumber.components = [ { name = my-script.lua, type = script/lua } ]

See also the overall 0.4 → 0.5 migration guide.

If you want to do anything vaguely interesting with your audio, you might find yourself wanting a way to create a loopback – a pair of nodes in the audio graph, where audio you put into one side will come out of the other side.

This was a very common thing to need with PulseAudio. With PipeWire and its ability to configure arbitrary numbers of inputs and outputs on everything, you can sometimes get away without them, but they're still useful in a number of cases, including compatibility with applications still using PulseAudio.

Unfortunately it's a massive pain to figure out how to set this up without just running pw-loopback (or even pactl load-module module-loopback) on login. Hopefully this page helps!

Things to avoid

Don't use libpipewire-module-combine-stream

There are many people online showing how they did things like this with libpipewire-module-combine-stream. If all you want is a loopback that never goes away, even if you want multiple audio streams to go into it, don't use libpipewire-module-combine-stream. It's designed for cases where you want to make a source that combines many sources into one (imagine you have a fancy array of microphones that exposes itself as many separate sources, but you just want A Single Microphone Source), or a sink that splits its input between many sinks (maybe you want to split the left and right audio channels from your music player over two separate speakers) – and crucially, it is designed to split/combine audio into or out of a static set of nodes. If any node attached to the business end of a module-combine-stream node goes away, the module-combine-stream node will be destroyed. (Imagine you unplugged your microphone array – you would want the virtual combined microphone to go away too!)

Don't try to set up context.objects by hand

It might theoretically be possible to achieve your goals by doing this, but writing things like factory.name = support.null-audio-sink is a pretty good sign that you're making things hard for yourself.

What you should do

Use libpipewire-module-loopback. It does everything you need! (Apart from connecting things up, but see the Linking section later for that.)

Here's an example configuration that creates one pair of loopback nodes (you could place this in e.g. /etc/pipewire/pipewire.conf.d/10-loopback.conf):

context.modules = [{
    name = libpipewire-module-loopback
    args = {
        audio.position = [ FL FR ]
        capture.props = {
            # This is the node that *captures* audio (the one with the audio input)
            media.class = "Audio/Sink"
            node.name = "my-loopback-sink"
            node.description = "Loopback input"
        }
        playback.props = {
            # This is the node that *plays* audio (the one with the audio output)
            media.class = "Stream/Output/Audio"
            node.name = "my-loopback-source"
            node.description = "Loopback output"
        }
    }
}]

What is absolutely critical to get right is the media.class configuration, which is "what kind of thing" each side of the loopback will pretend to be. The values above are reasonably safe defaults, but you could plausibly change each one of them to something else. To explain why, we have to talk about parallel universes PulseAudio.

PulseAudio's idea of the world

PulseAudio has four separate kinds of "thing" that can produce or consume audio:

Sources and sinks are obvious: a microphone is a source, and a speaker is a sink. PulseAudio calls these "devices".

Source outputs and sink inputs are slightly less obvious: a source output is "something a source can output to", i.e. an application recording audio, and a sink input is "something that produces input for a sink", i.e. an application playing audio. PulseAudio calls these "streams".

The device/stream dichotomy is a fundamental one in PulseAudio. A device can have any number of connections to or from it (multiple applications can record from the same microphone or play to the same speakers), but a stream can only be connected to one device (you can't pipe two audio sources into one application, or play sound from one application into two speakers, without extra effort). You also can't connect two devices, or two streams, directly together – so recording audio produced by an application, or listening to your own microphone's output, was challenging.

PipeWire's idea of the world

PipeWire treats everything, whether a piece of hardware or an application, as a "node". Nodes can have input and output "ports", and each port can be connected via "links" to any number of other ports. This means that with the help of a patchbay application like Helvum, you can do things like "pipe two audio sources into one application" without much difficulty at all!

Unfortunately, the device/stream distinction from PulseAudio is still lurking – firstly because you probably still use some applications that use PipeWire's PulseAudio compatibility layer, but secondly because WirePlumber (the thing that configures the default links between nodes) still uses basically the same ideas. The WirePlumber Linking Policy documentation, which explains how it decides which links to create by default, even uses the terms "stream node" and "device node"!

So, repeating the list of things that exist in PulseAudio's world, now with the media.class values PipeWire uses for them:

What this means in practice is that you need to think carefully about how many links each end of your loopback might have, and what should happen if the things linked to them disappear. If the input side of your loopback should be linked to your microphone if available, but to nothing if not, then it should be an Audio/Sink – if it's a Stream/Input/Audio, it'll get automatically connected to a fallback if your microphone disappears. Similarly, if you want the output side of your loopback to always be audible somewhere, then make it a Stream/Output/Audio, but if you'd rather it default to outputting to nowhere, pick Audio/Source. Applications are also more likely to include an Audio/Source in lists of microphones to record from than they are a Stream/Output/Audio.

Linking

What libpipewire-module-loopback doesn't handle is how to link your loopbacks to anything else. For that, you will need to configure WirePlumber, which means writing some Lua.

Rather than go over all the details, I'll point you towards an excellent post by Bennett Hardwick. With the helper functions he outlines, you can configure links between nodes as simply as this:

auto_connect_ports {
	output = Constraint { "port.alias", "matches", "My Microphone:*" },
	input = Constraint { "object.path", "matches", "my-loopback-sink:*" },
	connect = {
		["FL"] = "FL",
		["FR"] = "FR",
	},
}

I strongly recommend using Bennett's dump-ports.lua script to figure out how to refer to nodes from this file – pw-cli ls can be misleading in ways I don't fully understand.

Were I interested in contributing to WirePlumber, I would be quite tempted to try to include some kind of helper like auto_connect_ports upstream, to better support declarative approaches to configuring links. Alas, I am short on both time and Lua knowledge, so that will remain a hypothetical for now.

Note also that WirePlumber will make some links for you automatically, as described in Linking Policy; this might be helpful or annoying, depending on your aims. It might be possible to prevent WirePlumber interfering with your hand-crafted links by setting node.autoconnect to false.

Further reading

The PipeWire documentation is tricky to navigate, but it's not terrible. Some relevant pages include:

The WirePlumber documentation is substantially less comprehensive, but there are some useful pages:

There are also some great blog posts elsewhere: