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