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 = pulseaudioLight;
148 defaultText = "pkgs.pulseaudioLight";
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 environment.etc = [
218 { target = "asound.conf";
219 source = alsaConf; }
220
221 { target = "pulse/daemon.conf";
222 source = writeText "daemon.conf" (lib.generators.toKeyValue {} cfg.daemon.config); }
223
224 { target = "openal/alsoft.conf";
225 source = writeText "alsoft.conf" "drivers=pulse"; }
226
227 { target = "libao.conf";
228 source = writeText "libao.conf" "default_driver=pulse"; }
229 ];
230
231 # Allow PulseAudio to get realtime priority using rtkit.
232 security.rtkit.enable = true;
233
234 systemd.packages = [ overriddenPackage ];
235 })
236
237 (mkIf hasZeroconf {
238 services.avahi.enable = true;
239 })
240 (mkIf cfg.zeroconf.publish.enable {
241 services.avahi.publish.enable = true;
242 services.avahi.publish.userServices = true;
243 })
244
245 (mkIf nonSystemWide {
246 environment.etc = singleton {
247 target = "pulse/default.pa";
248 source = myConfigFile;
249 };
250 systemd.user = {
251 services.pulseaudio = {
252 restartIfChanged = true;
253 serviceConfig = {
254 RestartSec = "500ms";
255 PassEnvironment = "DISPLAY";
256 };
257 };
258 sockets.pulseaudio = {
259 wantedBy = [ "sockets.target" ];
260 };
261 };
262 })
263
264 (mkIf systemWide {
265 users.extraUsers.pulse = {
266 # For some reason, PulseAudio wants UID == GID.
267 uid = assert uid == gid; uid;
268 group = "pulse";
269 extraGroups = [ "audio" ];
270 description = "PulseAudio system service user";
271 home = stateDir;
272 createHome = true;
273 };
274
275 users.extraGroups.pulse.gid = gid;
276
277 systemd.services.pulseaudio = {
278 description = "PulseAudio System-Wide Server";
279 wantedBy = [ "sound.target" ];
280 before = [ "sound.target" ];
281 environment.PULSE_RUNTIME_PATH = stateDir;
282 serviceConfig = {
283 Type = "notify";
284 ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}";
285 Restart = "on-failure";
286 RestartSec = "500ms";
287 };
288 };
289
290 environment.variables.PULSE_COOKIE = "${stateDir}/.config/pulse/cookie";
291 })
292 ];
293
294}