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