1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8
9 cfg = config.services.pulseaudio;
10
11 hasZeroconf =
12 let
13 z = cfg.zeroconf;
14 in
15 z.publish.enable || z.discovery.enable;
16
17 overriddenPackage = cfg.package.override (
18 lib.optionalAttrs hasZeroconf { zeroconfSupport = true; }
19 );
20 binary = "${lib.getBin overriddenPackage}/bin/pulseaudio";
21 binaryNoDaemon = "${binary} --daemonize=no";
22
23 # Forces 32bit pulseaudio and alsa-plugins to be built/supported for apps
24 # using 32bit alsa on 64bit linux.
25 enable32BitAlsaPlugins =
26 cfg.support32Bit
27 && pkgs.stdenv.hostPlatform.isx86_64
28 && (pkgs.pkgsi686Linux.alsa-lib != null && pkgs.pkgsi686Linux.libpulseaudio != null);
29
30 myConfigFile =
31 let
32 addModuleIf = cond: mod: lib.optionalString cond "load-module ${mod}";
33 allAnon = lib.optional cfg.tcp.anonymousClients.allowAll "auth-anonymous=1";
34 ipAnon =
35 let
36 a = cfg.tcp.anonymousClients.allowedIpRanges;
37 in
38 lib.optional (a != [ ]) ''auth-ip-acl=${lib.concatStringsSep ";" a}'';
39 in
40 pkgs.writeTextFile {
41 name = "default.pa";
42 text = ''
43 .include ${cfg.configFile}
44 ${addModuleIf cfg.zeroconf.publish.enable "module-zeroconf-publish"}
45 ${addModuleIf cfg.zeroconf.discovery.enable "module-zeroconf-discover"}
46 ${addModuleIf cfg.tcp.enable (
47 lib.concatStringsSep " " ([ "module-native-protocol-tcp" ] ++ allAnon ++ ipAnon)
48 )}
49 ${addModuleIf config.services.jack.jackd.enable "module-jack-sink"}
50 ${addModuleIf config.services.jack.jackd.enable "module-jack-source"}
51 ${cfg.extraConfig}
52 '';
53 };
54
55 ids = config.ids;
56
57 uid = ids.uids.pulseaudio;
58 gid = ids.gids.pulseaudio;
59
60 stateDir = "/run/pulse";
61
62 # Create pulse/client.conf even if PulseAudio is disabled so
63 # that we can disable the autospawn feature in programs that
64 # are built with PulseAudio support (like KDE).
65 clientConf = pkgs.writeText "client.conf" ''
66 autospawn=no
67 ${cfg.extraClientConf}
68 '';
69
70 # Write an /etc/asound.conf that causes all ALSA applications to
71 # be re-routed to the PulseAudio server through ALSA's Pulse
72 # plugin.
73 alsaConf = ''
74 pcm_type.pulse {
75 libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;
76 ${lib.optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;"}
77 }
78 pcm.!default {
79 type pulse
80 hint.description "Default Audio Device (via PulseAudio)"
81 }
82 ctl_type.pulse {
83 libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;
84 ${lib.optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;"}
85 }
86 ctl.!default {
87 type pulse
88 }
89 '';
90
91in
92{
93 imports = [
94 (lib.mkRenamedOptionModule [ "hardware" "pulseaudio" ] [ "services" "pulseaudio" ])
95 ];
96
97 options = {
98
99 services.pulseaudio = {
100 enable = lib.mkOption {
101 type = lib.types.bool;
102 default = false;
103 description = ''
104 Whether to enable the PulseAudio sound server.
105 '';
106 };
107
108 systemWide = lib.mkOption {
109 type = lib.types.bool;
110 default = false;
111 description = ''
112 If false, a PulseAudio server is launched automatically for
113 each user that tries to use the sound system. The server runs
114 with user privileges. If true, one system-wide PulseAudio
115 server is launched on boot, running as the user "pulse", and
116 only users in the "pulse-access" group will have access to the server.
117 Please read the PulseAudio documentation for more details.
118
119 Don't enable this option unless you know what you are doing.
120 '';
121 };
122
123 support32Bit = lib.mkOption {
124 type = lib.types.bool;
125 default = false;
126 description = ''
127 Whether to include the 32-bit pulseaudio libraries in the system or not.
128 This is only useful on 64-bit systems and currently limited to x86_64-linux.
129 '';
130 };
131
132 configFile = lib.mkOption {
133 type = lib.types.nullOr lib.types.path;
134 description = ''
135 The path to the default configuration options the PulseAudio server
136 should use. By default, the "default.pa" configuration
137 from the PulseAudio distribution is used.
138 '';
139 };
140
141 extraConfig = lib.mkOption {
142 type = lib.types.lines;
143 default = "";
144 description = ''
145 Literal string to append to `configFile`
146 and the config file generated by the pulseaudio module.
147 '';
148 };
149
150 extraClientConf = lib.mkOption {
151 type = lib.types.lines;
152 default = "";
153 description = ''
154 Extra configuration appended to pulse/client.conf file.
155 '';
156 };
157
158 package = lib.mkOption {
159 type = lib.types.package;
160 default = if config.services.jack.jackd.enable then pkgs.pulseaudioFull else pkgs.pulseaudio;
161 defaultText = lib.literalExpression "pkgs.pulseaudio";
162 example = lib.literalExpression "pkgs.pulseaudioFull";
163 description = ''
164 The PulseAudio derivation to use. This can be used to enable
165 features (such as JACK support, Bluetooth) via the
166 `pulseaudioFull` package.
167 '';
168 };
169
170 extraModules = lib.mkOption {
171 type = lib.types.listOf lib.types.package;
172 default = [ ];
173 example = lib.literalExpression "[ pkgs.pulseaudio-modules-bt ]";
174 description = ''
175 Extra pulseaudio modules to use. This is intended for out-of-tree
176 pulseaudio modules like extra bluetooth codecs.
177
178 Extra modules take precedence over built-in pulseaudio modules.
179 '';
180 };
181
182 daemon = {
183 logLevel = lib.mkOption {
184 type = lib.types.str;
185 default = "notice";
186 description = ''
187 The log level that the system-wide pulseaudio daemon should use,
188 if activated.
189 '';
190 };
191
192 config = lib.mkOption {
193 type = lib.types.attrsOf lib.types.unspecified;
194 default = { };
195 description = "Config of the pulse daemon. See `man pulse-daemon.conf`.";
196 example = lib.literalExpression ''{ realtime-scheduling = "yes"; }'';
197 };
198 };
199
200 zeroconf = {
201 discovery.enable = lib.mkEnableOption "discovery of pulseaudio sinks in the local network";
202 publish.enable = lib.mkEnableOption "publishing the pulseaudio sink in the local network";
203 };
204
205 # TODO: enable by default?
206 tcp = {
207 enable = lib.mkEnableOption "tcp streaming support";
208
209 anonymousClients = {
210 allowAll = lib.mkEnableOption "all anonymous clients to stream to the server";
211 allowedIpRanges = lib.mkOption {
212 type = lib.types.listOf lib.types.str;
213 default = [ ];
214 example = lib.literalExpression ''[ "127.0.0.1" "192.168.1.0/24" ]'';
215 description = ''
216 A list of IP subnets that are allowed to stream to the server.
217 '';
218 };
219 };
220 };
221
222 };
223
224 };
225
226 config = lib.mkIf cfg.enable (
227 lib.mkMerge [
228 {
229 environment.etc."pulse/client.conf".source = clientConf;
230
231 environment.systemPackages = [ overriddenPackage ];
232
233 environment.etc = {
234 "alsa/conf.d/99-pulseaudio.conf".text = alsaConf;
235
236 "pulse/daemon.conf".source = pkgs.writeText "daemon.conf" (
237 lib.generators.toKeyValue { } cfg.daemon.config
238 );
239
240 "openal/alsoft.conf".source = pkgs.writeText "alsoft.conf" "drivers=pulse";
241
242 "libao.conf".source = pkgs.writeText "libao.conf" "default_driver=pulse";
243 };
244
245 services.pulseaudio.configFile = lib.mkDefault "${lib.getBin overriddenPackage}/etc/pulse/default.pa";
246
247 # Disable flat volumes to enable relative ones
248 services.pulseaudio.daemon.config.flat-volumes = lib.mkDefault "no";
249
250 # Upstream defaults to speex-float-1 which results in audible artifacts
251 services.pulseaudio.daemon.config.resample-method = lib.mkDefault "speex-float-5";
252
253 # Allow PulseAudio to get realtime priority using rtkit.
254 security.rtkit.enable = true;
255
256 systemd.packages = [ overriddenPackage ];
257
258 # PulseAudio is packaged with udev rules to handle various audio device quirks
259 services.udev.packages = [ overriddenPackage ];
260 }
261
262 (lib.mkIf (cfg.extraModules != [ ]) {
263 services.pulseaudio.daemon.config.dl-search-path =
264 let
265 overriddenModules = builtins.map (
266 drv: drv.override { pulseaudio = overriddenPackage; }
267 ) cfg.extraModules;
268 modulePaths =
269 builtins.map (drv: "${drv}/lib/pulseaudio/modules")
270 # User-provided extra modules take precedence
271 (overriddenModules ++ [ overriddenPackage ]);
272 in
273 lib.concatStringsSep ":" modulePaths;
274 })
275
276 (lib.mkIf hasZeroconf {
277 services.avahi.enable = true;
278 })
279 (lib.mkIf cfg.zeroconf.publish.enable {
280 services.avahi.publish.enable = true;
281 services.avahi.publish.userServices = true;
282 })
283
284 (lib.mkIf (!cfg.systemWide) {
285 environment.etc = {
286 "pulse/default.pa".source = myConfigFile;
287 };
288 systemd.user = {
289 services.pulseaudio =
290 {
291 restartIfChanged = true;
292 serviceConfig = {
293 RestartSec = "500ms";
294 PassEnvironment = "DISPLAY";
295 };
296 }
297 // lib.optionalAttrs config.services.jack.jackd.enable {
298 environment.JACK_PROMISCUOUS_SERVER = "jackaudio";
299 };
300 sockets.pulseaudio = {
301 wantedBy = [ "sockets.target" ];
302 };
303 };
304 })
305
306 (lib.mkIf cfg.systemWide {
307 users.users.pulse = {
308 # For some reason, PulseAudio wants UID == GID.
309 uid =
310 assert uid == gid;
311 uid;
312 group = "pulse";
313 extraGroups = [ "audio" ];
314 description = "PulseAudio system service user";
315 home = stateDir;
316 homeMode = "755";
317 createHome = true;
318 isSystemUser = true;
319 };
320
321 users.groups.pulse.gid = gid;
322 users.groups.pulse-access = { };
323
324 systemd.services.pulseaudio = {
325 description = "PulseAudio System-Wide Server";
326 wantedBy = [ "sound.target" ];
327 before = [ "sound.target" ];
328 environment.PULSE_RUNTIME_PATH = stateDir;
329 serviceConfig = {
330 Type = "notify";
331 ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}";
332 Restart = "on-failure";
333 RestartSec = "500ms";
334 };
335 };
336
337 environment.variables.PULSE_COOKIE = "${stateDir}/.config/pulse/cookie";
338 })
339 ]
340 );
341
342}