1{ config, lib, pkgs, pkgs_i686, ... }:
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 alsaPlugins to be built/supported for apps
21 # using 32bit alsa on 64bit linux.
22 enable32BitAlsaPlugins = cfg.support32Bit && stdenv.isx86_64 && (pkgs_i686.alsaLib != null && pkgs_i686.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 ${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=${if nonSystemWide then "yes" else "no"}
55 ${optionalString nonSystemWide "daemon-binary=${binary}"}
56 ${cfg.extraClientConf}
57 '';
58
59 # Write an /etc/asound.conf that causes all ALSA applications to
60 # be re-routed to the PulseAudio server through ALSA's Pulse
61 # plugin.
62 alsaConf = writeText "asound.conf" (''
63 pcm_type.pulse {
64 libs.native = ${pkgs.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;
65 ${lib.optionalString enable32BitAlsaPlugins
66 "libs.32Bit = ${pkgs_i686.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;"}
67 }
68 pcm.!default {
69 type pulse
70 hint.description "Default Audio Device (via PulseAudio)"
71 }
72 ctl_type.pulse {
73 libs.native = ${pkgs.alsaPlugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;
74 ${lib.optionalString enable32BitAlsaPlugins
75 "libs.32Bit = ${pkgs_i686.alsaPlugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;"}
76 }
77 ctl.!default {
78 type pulse
79 }
80 ${alsaCfg.extraConfig}
81 '');
82
83in {
84
85 options = {
86
87 hardware.pulseaudio = {
88 enable = mkOption {
89 type = types.bool;
90 default = false;
91 description = ''
92 Whether to enable the PulseAudio sound server.
93 '';
94 };
95
96 systemWide = mkOption {
97 type = types.bool;
98 default = false;
99 description = ''
100 If false, a PulseAudio server is launched automatically for
101 each user that tries to use the sound system. The server runs
102 with user privileges. This is the recommended and most secure
103 way to use PulseAudio. If true, one system-wide PulseAudio
104 server is launched on boot, running as the user "pulse", and
105 only users in the "audio" group will have access to the server.
106 Please read the PulseAudio documentation for more details.
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 <literal>configFile</literal>
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 = pkgs.pulseaudio;
148 defaultText = "pkgs.pulseaudio";
149 example = literalExample "pkgs.pulseaudioFull";
150 description = ''
151 The PulseAudio derivation to use. This can be used to enable
152 features (such as JACK support, Bluetooth) via the
153 <literal>pulseaudioFull</literal> package.
154 '';
155 };
156
157 daemon = {
158 logLevel = mkOption {
159 type = types.str;
160 default = "notice";
161 description = ''
162 The log level that the system-wide pulseaudio daemon should use,
163 if activated.
164 '';
165 };
166
167 config = mkOption {
168 type = types.attrsOf types.unspecified;
169 default = {};
170 description = ''Config of the pulse daemon. See <literal>man pulse-daemon.conf</literal>.'';
171 example = literalExample ''{ flat-volumes = "no"; }'';
172 };
173 };
174
175 zeroconf = {
176 discovery.enable =
177 mkEnableOption "discovery of pulseaudio sinks in the local network";
178 publish.enable =
179 mkEnableOption "publishing the pulseaudio sink in the local network";
180 };
181
182 # TODO: enable by default?
183 tcp = {
184 enable = mkEnableOption "tcp streaming support";
185
186 anonymousClients = {
187 allowAll = mkEnableOption "all anonymous clients to stream to the server";
188 allowedIpRanges = mkOption {
189 type = types.listOf types.str;
190 default = [];
191 example = literalExample ''[ "127.0.0.1" "192.168.1.0/24" ]'';
192 description = ''
193 A list of IP subnets that are allowed to stream to the server.
194 '';
195 };
196 };
197 };
198
199 };
200
201 };
202
203
204 config = mkMerge [
205 {
206 environment.etc = singleton {
207 target = "pulse/client.conf";
208 source = clientConf;
209 };
210
211 hardware.pulseaudio.configFile = mkDefault "${getBin overriddenPackage}/etc/pulse/default.pa";
212 }
213
214 (mkIf cfg.enable {
215 environment.systemPackages = [ overriddenPackage ];
216
217 sound.enable = true;
218
219 environment.etc = [
220 { target = "asound.conf";
221 source = alsaConf; }
222
223 { target = "pulse/daemon.conf";
224 source = writeText "daemon.conf" (lib.generators.toKeyValue {} cfg.daemon.config); }
225
226 { target = "openal/alsoft.conf";
227 source = writeText "alsoft.conf" "drivers=pulse"; }
228
229 { target = "libao.conf";
230 source = writeText "libao.conf" "default_driver=pulse"; }
231 ];
232
233 # Allow PulseAudio to get realtime priority using rtkit.
234 security.rtkit.enable = true;
235
236 systemd.packages = [ overriddenPackage ];
237 })
238
239 (mkIf hasZeroconf {
240 services.avahi.enable = true;
241 })
242 (mkIf cfg.zeroconf.publish.enable {
243 services.avahi.publish.enable = true;
244 services.avahi.publish.userServices = true;
245 })
246
247 (mkIf nonSystemWide {
248 environment.etc = singleton {
249 target = "pulse/default.pa";
250 source = myConfigFile;
251 };
252 systemd.user = {
253 services.pulseaudio = {
254 restartIfChanged = true;
255 serviceConfig = {
256 RestartSec = "500ms";
257 PassEnvironment = "DISPLAY";
258 };
259 };
260 sockets.pulseaudio = {
261 wantedBy = [ "sockets.target" ];
262 };
263 };
264 })
265
266 (mkIf systemWide {
267 users.users.pulse = {
268 # For some reason, PulseAudio wants UID == GID.
269 uid = assert uid == gid; uid;
270 group = "pulse";
271 extraGroups = [ "audio" ];
272 description = "PulseAudio system service user";
273 home = stateDir;
274 createHome = true;
275 };
276
277 users.groups.pulse.gid = gid;
278
279 systemd.services.pulseaudio = {
280 description = "PulseAudio System-Wide Server";
281 wantedBy = [ "sound.target" ];
282 before = [ "sound.target" ];
283 environment.PULSE_RUNTIME_PATH = stateDir;
284 serviceConfig = {
285 Type = "notify";
286 ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}";
287 Restart = "on-failure";
288 RestartSec = "500ms";
289 };
290 };
291
292 environment.variables.PULSE_COOKIE = "${stateDir}/.config/pulse/cookie";
293 })
294 ];
295
296}