1# PipeWire service.
2{
3 config,
4 lib,
5 pkgs,
6 ...
7}:
8
9let
10 inherit (builtins) attrNames concatMap length;
11 inherit (lib) maintainers teams;
12 inherit (lib.attrsets)
13 attrByPath
14 attrsToList
15 concatMapAttrs
16 filterAttrs
17 ;
18 inherit (lib.lists) flatten optional optionals;
19 inherit (lib.modules) mkIf mkRemovedOptionModule;
20 inherit (lib.options)
21 literalExpression
22 mkEnableOption
23 mkOption
24 mkPackageOption
25 ;
26 inherit (lib.strings) concatMapStringsSep hasPrefix optionalString;
27 inherit (lib.types)
28 attrsOf
29 bool
30 listOf
31 package
32 ;
33
34 json = pkgs.formats.json { };
35 mapToFiles =
36 location: config:
37 concatMapAttrs (name: value: {
38 "share/pipewire/${location}.conf.d/${name}.conf" = json.generate "${name}" value;
39 }) config;
40 extraConfigPkgFromFiles =
41 locations: filesSet:
42 pkgs.runCommand "pipewire-extra-config" { } ''
43 mkdir -p ${concatMapStringsSep " " (l: "$out/share/pipewire/${l}.conf.d") locations}
44 ${concatMapStringsSep ";" ({ name, value }: "ln -s ${value} $out/${name}") (attrsToList filesSet)}
45 '';
46 cfg = config.services.pipewire;
47 enable32BitAlsaPlugins =
48 cfg.alsa.support32Bit && pkgs.stdenv.hostPlatform.isx86_64 && pkgs.pkgsi686Linux.pipewire != null;
49
50 # The package doesn't output to $out/lib/pipewire directly so that the
51 # overlays can use the outputs to replace the originals in FHS environments.
52 #
53 # This doesn't work in general because of missing development information.
54 jack-libs = pkgs.runCommand "jack-libs" { } ''
55 mkdir -p "$out/lib"
56 ln -s "${cfg.package.jack}/lib" "$out/lib/pipewire"
57 '';
58
59 configPackages = cfg.configPackages;
60
61 extraConfigPkg = extraConfigPkgFromFiles [ "pipewire" "client" "jack" "pipewire-pulse" ] (
62 mapToFiles "pipewire" cfg.extraConfig.pipewire
63 // mapToFiles "client" cfg.extraConfig.client
64 // mapToFiles "jack" cfg.extraConfig.jack
65 // mapToFiles "pipewire-pulse" cfg.extraConfig.pipewire-pulse
66 );
67
68 configs = pkgs.buildEnv {
69 name = "pipewire-configs";
70 paths =
71 configPackages
72 ++ [ extraConfigPkg ]
73 ++ optionals cfg.wireplumber.enable cfg.wireplumber.configPackages;
74 pathsToLink = [ "/share/pipewire" ];
75 };
76
77 requiredLv2Packages = flatten (
78 concatMap (p: attrByPath [ "passthru" "requiredLv2Packages" ] [ ] p) configPackages
79 );
80
81 lv2Plugins = pkgs.buildEnv {
82 name = "pipewire-lv2-plugins";
83 paths = cfg.extraLv2Packages ++ requiredLv2Packages;
84 pathsToLink = [ "/lib/lv2" ];
85 };
86in
87{
88 meta.maintainers = teams.freedesktop.members ++ [ maintainers.k900 ];
89
90 ###### interface
91 options = {
92 services.pipewire = {
93 enable = mkEnableOption "PipeWire service";
94
95 package = mkPackageOption pkgs "pipewire" { };
96
97 socketActivation = mkOption {
98 default = true;
99 type = bool;
100 description = ''
101 Automatically run PipeWire when connections are made to the PipeWire socket.
102 '';
103 };
104
105 audio = {
106 enable = mkOption {
107 type = bool;
108 # this is for backwards compatibility
109 default = cfg.alsa.enable || cfg.jack.enable || cfg.pulse.enable;
110 defaultText = literalExpression "config.services.pipewire.alsa.enable || config.services.pipewire.jack.enable || config.services.pipewire.pulse.enable";
111 description = "Whether to use PipeWire as the primary sound server";
112 };
113 };
114
115 alsa = {
116 enable = mkEnableOption "ALSA support";
117 support32Bit = mkEnableOption "32-bit ALSA support on 64-bit systems";
118 };
119
120 jack = {
121 enable = mkEnableOption "JACK audio emulation";
122 };
123
124 raopOpenFirewall = mkOption {
125 type = bool;
126 default = false;
127 description = ''
128 Opens UDP/6001-6002, required by RAOP/Airplay for timing and control data.
129 '';
130 };
131
132 pulse = {
133 enable = mkEnableOption "PulseAudio server emulation";
134 };
135
136 systemWide = mkOption {
137 type = bool;
138 default = false;
139 description = ''
140 If true, a system-wide PipeWire service and socket is enabled
141 allowing all users in the "pipewire" group to use it simultaneously.
142 If false, then user units are used instead, restricting access to
143 only one user.
144
145 Enabling system-wide PipeWire is however not recommended and disabled
146 by default according to
147 https://github.com/PipeWire/pipewire/blob/master/NEWS
148 '';
149 };
150
151 extraConfig = {
152 pipewire = mkOption {
153 type = attrsOf json.type;
154 default = { };
155 example = {
156 "10-clock-rate" = {
157 "context.properties" = {
158 "default.clock.rate" = 44100;
159 };
160 };
161 "11-no-upmixing" = {
162 "stream.properties" = {
163 "channelmix.upmix" = false;
164 };
165 };
166 };
167 description = ''
168 Additional configuration for the PipeWire server.
169
170 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/pipewire.conf.d`.
171
172 See `man pipewire.conf` for details, and [the PipeWire wiki][wiki] for examples.
173
174 See also:
175 - [PipeWire wiki - virtual devices][wiki-virtual-device] for creating virtual devices or remapping channels
176 - [PipeWire wiki - filter-chain][wiki-filter-chain] for creating more complex processing pipelines
177 - [PipeWire wiki - network][wiki-network] for streaming audio over a network
178
179 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PipeWire
180 [wiki-virtual-device]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Virtual-Devices
181 [wiki-filter-chain]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Filter-Chain
182 [wiki-network]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Network
183 '';
184 };
185 client = mkOption {
186 type = attrsOf json.type;
187 default = { };
188 example = {
189 "10-no-resample" = {
190 "stream.properties" = {
191 "resample.disable" = true;
192 };
193 };
194 };
195 description = ''
196 Additional configuration for the PipeWire client library, used by most applications.
197
198 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/client.conf.d`.
199
200 See the [PipeWire wiki][wiki] for examples.
201
202 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-client
203 '';
204 };
205 jack = mkOption {
206 type = attrsOf json.type;
207 default = { };
208 example = {
209 "20-hide-midi" = {
210 "jack.properties" = {
211 "jack.show-midi" = false;
212 };
213 };
214 };
215 description = ''
216 Additional configuration for the PipeWire JACK server and client library.
217
218 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/jack.conf.d`.
219
220 See the [PipeWire wiki][wiki] for examples.
221
222 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-JACK
223 '';
224 };
225 pipewire-pulse = mkOption {
226 type = attrsOf json.type;
227 default = { };
228 example = {
229 "15-force-s16-info" = {
230 "pulse.rules" = [
231 {
232 matches = [
233 { "application.process.binary" = "my-broken-app"; }
234 ];
235 actions = {
236 quirks = [ "force-s16-info" ];
237 };
238 }
239 ];
240 };
241 };
242 description = ''
243 Additional configuration for the PipeWire PulseAudio server.
244
245 Every item in this attrset becomes a separate drop-in file in `/etc/pipewire/pipewire-pulse.conf.d`.
246
247 See `man pipewire-pulse.conf` for details, and [the PipeWire wiki][wiki] for examples.
248
249 See also:
250 - [PipeWire wiki - PulseAudio tricks guide][wiki-tricks] for more examples.
251
252 [wiki]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Config-PulseAudio
253 [wiki-tricks]: https://gitlab.freedesktop.org/pipewire/pipewire/-/wikis/Guide-PulseAudio-Tricks
254 '';
255 };
256 };
257
258 configPackages = mkOption {
259 type = listOf package;
260 default = [ ];
261 example = literalExpression ''
262 [
263 (pkgs.writeTextDir "share/pipewire/pipewire.conf.d/10-loopback.conf" '''
264 context.modules = [
265 { name = libpipewire-module-loopback
266 args = {
267 node.description = "Scarlett Focusrite Line 1"
268 capture.props = {
269 audio.position = [ FL ]
270 stream.dont-remix = true
271 node.target = "alsa_input.usb-Focusrite_Scarlett_Solo_USB_Y7ZD17C24495BC-00.analog-stereo"
272 node.passive = true
273 }
274 playback.props = {
275 node.name = "SF_mono_in_1"
276 media.class = "Audio/Source"
277 audio.position = [ MONO ]
278 }
279 }
280 }
281 ]
282 ''')
283 ]'';
284 description = ''
285 List of packages that provide PipeWire configuration, in the form of
286 `share/pipewire/*/*.conf` files.
287
288 LV2 dependencies will be picked up from config packages automatically
289 via `passthru.requiredLv2Packages`.
290 '';
291 };
292
293 extraLv2Packages = mkOption {
294 type = listOf package;
295 default = [ ];
296 example = literalExpression "[ pkgs.lsp-plugins ]";
297 description = ''
298 List of packages that provide LV2 plugins in `lib/lv2` that should
299 be made available to PipeWire for [filter chains][wiki-filter-chain].
300
301 Config packages have their required LV2 plugins added automatically,
302 so they don't need to be specified here. Config packages need to set
303 `passthru.requiredLv2Packages` for this to work.
304
305 [wiki-filter-chain]: https://docs.pipewire.org/page_module_filter_chain.html
306 '';
307 };
308 };
309 };
310
311 imports = [
312 (mkRemovedOptionModule [ "services" "pipewire" "config" ] ''
313 Overriding default PipeWire configuration through NixOS options never worked correctly and is no longer supported.
314 Please create drop-in configuration files via `services.pipewire.extraConfig` instead.
315 '')
316 (mkRemovedOptionModule [ "services" "pipewire" "media-session" ] ''
317 pipewire-media-session is no longer supported upstream and has been removed.
318 Please switch to `services.pipewire.wireplumber` instead.
319 '')
320 (mkRemovedOptionModule [ "services" "pipewire" "extraConfig" "client-rt" ] ''
321 `services.pipewire.extraConfig.client-rt` is no longer applicable, as `client-rt.conf` has been
322 removed upstream. Please move your customizations to `services.pipewire.extraConfig.client`.
323 '')
324 ];
325
326 ###### implementation
327 config = mkIf cfg.enable {
328 assertions = [
329 {
330 assertion = cfg.audio.enable -> !config.services.pulseaudio.enable;
331 message = "Using PipeWire as the sound server conflicts with PulseAudio. This option requires `services.pulseaudio.enable` to be set to false";
332 }
333 {
334 assertion = cfg.jack.enable -> !config.services.jack.jackd.enable;
335 message = "PipeWire based JACK emulation doesn't use the JACK service. This option requires `services.jack.jackd.enable` to be set to false";
336 }
337 {
338 # JACK intentionally not checked, as PW-on-JACK setups are a thing that some people may want
339 assertion = (cfg.alsa.enable || cfg.pulse.enable) -> cfg.audio.enable;
340 message = "Using PipeWire's ALSA/PulseAudio compatibility layers requires running PipeWire as the sound server. Either set `services.pipewire.audio.enable` to true to enable audio support, or set both `services.pipewire.pulse.enable` and `services.pipewire.alsa.enable` to false to use pipewire exclusively for the compositor.";
341 }
342 {
343 assertion =
344 length (
345 attrNames (
346 filterAttrs (name: value: hasPrefix "pipewire/" name || name == "pipewire") config.environment.etc
347 )
348 ) == 1;
349 message = "Using `environment.etc.\"pipewire<...>\"` directly is no longer supported. Use `services.pipewire.extraConfig` or `services.pipewire.configPackages` instead.";
350 }
351 ];
352
353 environment.systemPackages = [ cfg.package ] ++ optional cfg.jack.enable jack-libs;
354
355 systemd.packages = [ cfg.package ];
356
357 # PipeWire depends on DBUS but doesn't list it. Without this booting
358 # into a terminal results in the service crashing with an error.
359 systemd.services.pipewire.bindsTo = [ "dbus.service" ];
360 systemd.user.services.pipewire.bindsTo = [ "dbus.service" ];
361
362 # Enable either system or user units.
363 systemd.sockets.pipewire.enable = cfg.systemWide;
364 systemd.services.pipewire.enable = cfg.systemWide;
365 systemd.user.sockets.pipewire.enable = !cfg.systemWide;
366 systemd.user.services.pipewire.enable = !cfg.systemWide;
367
368 systemd.services.pipewire.environment.LV2_PATH = mkIf cfg.systemWide "${lv2Plugins}/lib/lv2";
369 systemd.user.services.pipewire.environment.LV2_PATH = mkIf (
370 !cfg.systemWide
371 ) "${lv2Plugins}/lib/lv2";
372 systemd.user.services.filter-chain.environment.LV2_PATH = mkIf (
373 !cfg.systemWide
374 ) "${lv2Plugins}/lib/lv2";
375
376 # Mask pw-pulse if it's not wanted
377 systemd.services.pipewire-pulse.enable = cfg.pulse.enable && cfg.systemWide;
378 systemd.sockets.pipewire-pulse.enable = cfg.pulse.enable && cfg.systemWide;
379 systemd.user.services.pipewire-pulse.enable = cfg.pulse.enable && !cfg.systemWide;
380 systemd.user.sockets.pipewire-pulse.enable = cfg.pulse.enable && !cfg.systemWide;
381
382 systemd.sockets.pipewire.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ];
383 systemd.sockets.pipewire-pulse.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ];
384 systemd.user.sockets.pipewire.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ];
385 systemd.user.sockets.pipewire-pulse.wantedBy = mkIf cfg.socketActivation [ "sockets.target" ];
386
387 services.udev.packages = [ cfg.package ];
388
389 # If any paths are updated here they must also be updated in the package test.
390 environment.etc = {
391 "alsa/conf.d/49-pipewire-modules.conf" = mkIf cfg.alsa.enable {
392 text = ''
393 pcm_type.pipewire {
394 libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;
395 ${optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire}/lib/alsa-lib/libasound_module_pcm_pipewire.so ;"}
396 }
397 ctl_type.pipewire {
398 libs.native = ${cfg.package}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;
399 ${optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.pipewire}/lib/alsa-lib/libasound_module_ctl_pipewire.so ;"}
400 }
401 '';
402 };
403
404 "alsa/conf.d/50-pipewire.conf" = mkIf cfg.alsa.enable {
405 source = "${cfg.package}/share/alsa/alsa.conf.d/50-pipewire.conf";
406 };
407
408 "alsa/conf.d/99-pipewire-default.conf" = mkIf cfg.alsa.enable {
409 source = "${cfg.package}/share/alsa/alsa.conf.d/99-pipewire-default.conf";
410 };
411 pipewire.source = "${configs}/share/pipewire";
412 };
413
414 environment.sessionVariables.LD_LIBRARY_PATH = mkIf cfg.jack.enable [ "${cfg.package.jack}/lib" ];
415
416 networking.firewall.allowedUDPPorts = mkIf cfg.raopOpenFirewall [
417 6001
418 6002
419 ];
420
421 # See https://gitlab.freedesktop.org/pipewire/pipewire/-/blob/master/src/modules/module-rt/25-pw-rlimits.conf.in
422 security.pam.loginLimits = [
423 {
424 domain = "@pipewire";
425 item = "rtprio";
426 type = "-";
427 value = 95;
428 }
429 {
430 domain = "@pipewire";
431 item = "nice";
432 type = "-";
433 value = -19;
434 }
435 {
436 domain = "@pipewire";
437 item = "memlock";
438 type = "-";
439 value = 4194304;
440 }
441 ];
442
443 users = {
444 users.pipewire = mkIf cfg.systemWide {
445 uid = config.ids.uids.pipewire;
446 group = "pipewire";
447 extraGroups = [
448 "audio"
449 "video"
450 ]
451 ++ optional config.security.rtkit.enable "rtkit";
452 description = "PipeWire system service user";
453 isSystemUser = true;
454 home = "/var/lib/pipewire";
455 createHome = true;
456 };
457 groups.pipewire.gid = config.ids.gids.pipewire;
458 };
459 };
460}