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