PipeWire
PipeWire is a relatively new (first release in 2017) low-level multimedia framework. It aims to offer capture and playback for both audio and video with minimal latency and support for PulseAudio-, JACK-, ALSA- and GStreamer-based applications. PipeWire has a great bluetooth support: because Pulseaudio was reported to have troubles with bluetooth, PipeWire can be a good alternative.
The daemon based on the framework can be configured to be both an audio server (with PulseAudio and JACK features) and a video capture server.
PipeWire also supports containers like Flatpak and does not rely on audio and video user groups but rather it uses a Polkit-like security model asking Flatpak or Wayland for permission to record screen or audio.
Enabling PipeWire
PipeWire can be enabled with the following configuration:
# Remove sound.enable or set it to false if you had it set previously, as sound.enable is only meant for ALSA-based configurations
# rtkit is optional but recommended
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
};
It is possible to use theervices.pipewire.extraConfig
option hierarchy in NixOS (available from 24.05 onwards) to create drop-in configuration files, if needed.
Bluetooth Configuration
PipeWire can be configured to use specific codecs. The mSBC codec provides slightly better sound quality in calls than regular HFP/HSP, while the SBC-XQ provides better sound quality for audio listening. For more information see this link.
Wireplumber (services.pipewire.wireplumber
) is the default modular session / policy manager for PipeWire in unstable, however there is currently no way to configure the advanced settings using the module. However, with 24.05 you can use services.pipewire.wireplumber.configPackages
to create drop-in Lua files directly in the expected path (/etc/wireplumber/bluetooth.lua.d
). For example:
services.pipewire.wireplumber.configPackages = [
(pkgs.writeTextDir "share/wireplumber/bluetooth.lua.d/51-bluez-config.lua" ''
bluez_monitor.properties = {
["bluez5.enable-sbc-xq"] = true,
["bluez5.enable-msbc"] = true,
["bluez5.enable-hw-volume"] = true,
["bluez5.headset-roles"] = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
}
'')
];
Alternatively you can set services.pipewire.wireplumber.extraLuaConfig
as well:
services.pipewire.wireplumber.extraLuaConfig.bluetooth."51-bluez-config" = ''
bluez_monitor.properties = {
["bluez5.enable-sbc-xq"] = true,
["bluez5.enable-msbc"] = true,
["bluez5.enable-hw-volume"] = true,
["bluez5.headset-roles"] = "[ hsp_hs hsp_ag hfp_hf hfp_ag ]"
}
'';
</syntaxHighlight>
It is possible change a particular user instead of system-wide, with adding this to ~/.config/wireplumber/bluetooth.lua.d
instead, manually or using Home-Manager.
If you're still on 21.11 or enabled pipewire-media-session
manually (by setting services.pipewire.media-session.enable = true
), them you can use the module to configure it:
services.pipewire = {
media-session.config.bluez-monitor.rules = [
{
# Matches all cards
matches = [ { "device.name" = "~bluez_card.*"; } ];
actions = {
"update-props" = {
"bluez5.reconnect-profiles" = [ "hfp_hf" "hsp_hs" "a2dp_sink" ];
# mSBC is not expected to work on all headset + adapter combinations.
"bluez5.msbc-support" = true;
# SBC-XQ is not expected to work on all headset + adapter combinations.
"bluez5.sbc-xq-support" = true;
};
};
}
{
matches = [
# Matches all sources
{ "node.name" = "~bluez_input.*"; }
# Matches all outputs
{ "node.name" = "~bluez_output.*"; }
];
}
];
};
Graphical tools
All protocols (Pulseaudio/JACK) are now talking to the PipeWire protocol and are managed by the PipeWire daemon (therefore, applications can be managed by both Pulseaudio and JACK tools). For that reason, all graphical tools used for these protocols can be used:
- pavucontrol: controls the volume (per-sink and per-app basis), the default outputs/inputs, the different profiles (for HDMI outputs/bluetooth devices), routes each application to a different input/output, etc.
- plasma-pa: a Plasma applet to change volume directly from the systray. Also deals with volume keys.
- qjackctl: with JACK emulation, provides a patchbay (to connect applications together). Note that JACK does not provide any way to change the volume of a single application; use Pulseaudio tools for that purpose.
- carla: with JACK emulation, provides a patchbay (make sure to go to "Patchbay" tab and check "Canvas > Show External").
- catia/patchage: similar to qjackctl and carla.
- Helvum: GTK-based patchbay for PipeWire (uses the PipeWire protocol). Volume control is planned for later.
Advanced Configuration
PipeWire can be extensively configured to fit the users' needs. Should the user want to do some fancy routing with null sinks, these can be defined directly in the config as shown below.
This is especially convenient if the user has a multi-channel (8+, or something "weird" like 2x2, 3x2) soundcard that keeps confusing applications with too many channels or a bad channel layout.
Note: those cards can be set to the "Pro Audio" profile with pavucontrol
so PipeWire doesn't try to guess a wrong channel layout for them.
For NixOS 24.05 and newer:
services.pipewire.extraConfig.pipewire."91-null-sinks" = {
"context.objects" = [
{
# A default dummy driver. This handles nodes marked with the "node.always-driver"
# properyty when no other driver is currently active. JACK clients need this.
factory = "spa-node-factory";
args = {
"factory.name" = "support.node.driver";
"node.name" = "Dummy-Driver";
"priority.driver" = 8000;
};
}
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "Microphone-Proxy";
"node.description" = "Microphone";
"media.class" = "Audio/Source/Virtual";
"audio.position" = "MONO";
};
}
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "Main-Output-Proxy";
"node.description" = "Main Output";
"media.class" = "Audio/Sink";
"audio.position" = "FL,FR";
};
}
];
};
If you're still using 23.11 or earlier, you can use environment.etc
and pkgs.formats.json
:
environment.etc = let
json = pkgs.formats.json {};
in {
"pipewire/pipewire.d/91-null-sinks.conf".source = json.generate "91-null-sinks.conf" {
"context.objects" = [
{
# A default dummy driver. This handles nodes marked with the "node.always-driver"
# properyty when no other driver is currently active. JACK clients need this.
factory = "spa-node-factory";
args = {
"factory.name" = "support.node.driver";
"node.name" = "Dummy-Driver";
"priority.driver" = 8000;
};
}
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "Microphone-Proxy";
"node.description" = "Microphone";
"media.class" = "Audio/Source/Virtual";
"audio.position" = "MONO";
};
}
{
factory = "adapter";
args = {
"factory.name" = "support.null-audio-sink";
"node.name" = "Main-Output-Proxy";
"node.description" = "Main Output";
"media.class" = "Audio/Sink";
"audio.position" = "FL,FR";
};
}
];
};
};
Linking nodes
The config does not currently cover linking nodes together, but this can be fixed with a script. Soundcard names and ports should be replaced with the ones from the user's configuration:
#!/usr/bin/env bash
# ports obtained from `pw-link -io`
pw-link "Main-Output-Proxy:monitor_FL" "alsa_output.usb-Native_Instruments_Komplete_Audio_6_69BC86B9-00.pro-audio:playback_1"
pw-link "Main-Output-Proxy:monitor_FR" "alsa_output.usb-Native_Instruments_Komplete_Audio_6_69BC86B9-00.pro-audio:playback_2"
pw-link "alsa_input.usb-M-Audio_Fast_Track-00.pro-audio:capture_1" "Microphone-Proxy:input_MONO"
In order to load the script on startup, it can be added to ~/.xprofile
or the specific DE/WM autostart config. Similarly, a one-shot user service can be created that runs the script.
Low-latency setup
Audio production and rhythm games require lower latency audio than general applications. PipeWire can achieve the required latency with much less CPU usage compared to PulseAudio, with the appropriate configuration. The minimum period size controls how small a buffer can be. The lower it is, the less latency there is. PipeWire has a value of 32/48000 by default, which amounts to 0.667ms. It can be brought lower if needed: For 24.05 and newer:
services.pipewire.extraConfig.pipewire."92-low-latency" = {
"context.properties" = {
"default.clock.rate" = 48000;
"default.clock.quantum" = 32;
"default.clock.min-quantum" = 32;
"default.clock.max-quantum" = 32;
};
};
If you're still using 23.11 or earlier, you can use environment.etc
and pkgs.formats.json
like in Advanced Configuration.
NOTE: Every setup is different, and a lot of factors determine your final latency, like CPU speed, RT/PREEMPTIVE kernels and soundcards supporting different audio formats. That's why 32/48000 isn't always a value that's going to work for everyone. The best way to get everything working is to keep increasing the quant value until you get no crackles (underruns) or until you get audio again (in case there wasn't any). This won't guarantee the lowest possible latency, but will provide a decent one paired with stable audio.
PulseAudio backend
Applications using the Pulse backend have a separate configuration. The default minimum value is 1024, so it needs to be tweaked if low-latency audio is desired. For 24.05 or newer:
services.pipewire.extraConfig.pipewire-pulse."92-low-latency" = {
"context.properties" = [
{
name = "libpipewire-module-protocol-pulse";
args = { };
}
];
"pulse.properties" = {
"pulse.min.req" = "32/48000";
"pulse.default.req" = "32/48000";
"pulse.max.req" = "32/48000";
"pulse.min.quantum" = "32/48000";
"pulse.max.quantum" = "32/48000";
};
"stream.properties" = {
"node.latency" = "32/48000";
"resample.quality" = 1;
};
};
If you're still using 23.11 or earlier, you can use environment.etc
and pkgs.formats.json
like in Advanced Configuration.
As a general rule, the values in pipewire-pulse
should not be lower than the ones in pipewire
.
Controlling the ALSA devices
It is possible to configure various aspects of soundcards through PipeWire, including format, period size and batch mode: For 24.05 and newer:
services.pipewire.wireplumber.configPackages = [
(pkgs.writeTextDir "share/wireplumber/main.lua.d/99-alsa-lowlatency.lua" ''
alsa_monitor.rules = {
{
matches = {{{ "node.name", "matches", "alsa_output.*" }}};
apply_properties = {
["audio.format"] = "S32LE",
["audio.rate"] = "96000", -- for USB soundcards it should be twice your desired rate
["api.alsa.period-size"] = 2, -- defaults to 1024, tweak by trial-and-error
-- ["api.alsa.disable-batch"] = true, -- generally, USB soundcards use the batch mode
},
},
}
'')
];
Once #292115 is merged and has reached nixos-unstable, you'll be able to use services.pipewire.wireplumber.extraLuaConfig
as well:
services.pipewire.wireplumber.extraLuaConfig.main."99-alsa-lowlatency" = ''
alsa_monitor.rules = {
{
matches = {{{ "node.name", "matches", "alsa_output.*" }}};
apply_properties = {
["audio.format"] = "S32LE",
["audio.rate"] = "96000", -- for USB soundcards it should be twice your desired rate
["api.alsa.period-size"] = 2, -- defaults to 1024, tweak by trial-and-error
-- ["api.alsa.disable-batch"] = true, -- generally, USB soundcards use the batch mode
},
},
}
'';
If you're still using 23.11 or earlier, you can use environment.etc
:
environment.etc.
"wireplumber/main.lua.d/99-alsa-lowlatency.lua".text = ''
alsa_monitor.rules = {
{
matches = {{{ "node.name", "matches", "alsa_output.*" }}};
apply_properties = {
["audio.format"] = "S32LE",
["audio.rate"] = "96000", -- for USB soundcards it should be twice your desired rate
["api.alsa.period-size"] = 2, -- defaults to 1024, tweak by trial-and-error
-- ["api.alsa.disable-batch"] = true, -- generally, USB soundcards use the batch mode
},
},
}
'';
The matches
attribute applies the actions
to the devices/properties listed there. It is usually used with soundcard names, like shown in the config above. <matches>
can match any of the outputs of
$ pw-dump | grep node.name | grep alsa
Troubleshooting
pactl not found
The pactl
functionality is superseded in PipeWire with the native pw-cli
, pw-mon
and pw-top
CLI tools.
When using WirePlumber (which is enabled by default), you can also use wpctl
as a pactl
alternative with similar high level subcommands.
No signal detected in audio application from external audio interface
This issue was seen when attempting to use a Roland STUDIO-CAPTURE 16x10 Audio Interface to record audio in Ardour8 on NixOS. One possible solution:
- Install
pavucontrol
. - Run
pavucontrol
, navigate to the configuration tab, and set the Profile for STUDIO-CAPTURE (or your audio interface) to "Pro Audio". - Ensure the audio interface and Ardour are set to the same sample rate. Ardour8 will accept a configuration where its sample rate does not match the audio interface's sample rate, when using PipeWire, but will not actually be able to record audio unless they match. If you are unable to successfully activate a track's record button, this may be the issue.