1{ config, lib, pkgs, pkgs_i686, ... }:
2
3with pkgs;
4with lib;
5
6let
7
8 cfg = config.hardware.pulseaudio;
9
10 systemWide = cfg.enable && cfg.systemWide;
11 nonSystemWide = cfg.enable && !cfg.systemWide;
12 hasZeroconf = let z = cfg.zeroconf; in z.publish.enable || z.discovery.enable;
13
14 overriddenPackage = cfg.package.override
15 (optionalAttrs hasZeroconf { zeroconfSupport = true; });
16 binary = "${getBin overriddenPackage}/bin/pulseaudio";
17 binaryNoDaemon = "${binary} --daemonize=no";
18
19 # Forces 32bit pulseaudio and alsaPlugins to be built/supported for apps
20 # using 32bit alsa on 64bit linux.
21 enable32BitAlsaPlugins = cfg.support32Bit && stdenv.isx86_64 && (pkgs_i686.alsaLib != null && pkgs_i686.libpulseaudio != null);
22
23
24 myConfigFile =
25 let
26 addModuleIf = cond: mod: optionalString cond "load-module ${mod}";
27 allAnon = optional cfg.tcp.anonymousClients.allowAll "auth-anonymous=1";
28 ipAnon = let a = cfg.tcp.anonymousClients.allowedIpRanges;
29 in optional (a != []) ''auth-ip-acl=${concatStringsSep ";" a}'';
30 in writeTextFile {
31 name = "default.pa";
32 text = ''
33 .include ${cfg.configFile}
34 ${addModuleIf cfg.zeroconf.publish.enable "module-zeroconf-publish"}
35 ${addModuleIf cfg.zeroconf.discovery.enable "module-zeroconf-discover"}
36 ${addModuleIf cfg.tcp.enable (concatStringsSep " "
37 ([ "module-native-protocol-tcp" ] ++ allAnon ++ ipAnon))}
38 ${cfg.extraConfig}
39 '';
40 };
41
42 ids = config.ids;
43
44 uid = ids.uids.pulseaudio;
45 gid = ids.gids.pulseaudio;
46
47 stateDir = "/var/run/pulse";
48
49 # Create pulse/client.conf even if PulseAudio is disabled so
50 # that we can disable the autospawn feature in programs that
51 # are built with PulseAudio support (like KDE).
52 clientConf = writeText "client.conf" ''
53 autospawn=${if nonSystemWide then "yes" else "no"}
54 ${optionalString nonSystemWide "daemon-binary=${binary}"}
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.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;
64 ${lib.optionalString enable32BitAlsaPlugins
65 "libs.32Bit = ${pkgs_i686.alsaPlugins}/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.alsaPlugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;
73 ${lib.optionalString enable32BitAlsaPlugins
74 "libs.32Bit = ${pkgs_i686.alsaPlugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;"}
75 }
76 ctl.!default {
77 type pulse
78 }
79 '');
80
81in {
82
83 options = {
84
85 hardware.pulseaudio = {
86 enable = mkOption {
87 type = types.bool;
88 default = false;
89 description = ''
90 Whether to enable the PulseAudio sound server.
91 '';
92 };
93
94 systemWide = mkOption {
95 type = types.bool;
96 default = false;
97 description = ''
98 If false, a PulseAudio server is launched automatically for
99 each user that tries to use the sound system. The server runs
100 with user privileges. This is the recommended and most secure
101 way to use PulseAudio. If true, one system-wide PulseAudio
102 server is launched on boot, running as the user "pulse".
103 Please read the PulseAudio documentation for more details.
104 '';
105 };
106
107 support32Bit = mkOption {
108 type = types.bool;
109 default = false;
110 description = ''
111 Whether to include the 32-bit pulseaudio libraries in the systemn or not.
112 This is only useful on 64-bit systems and currently limited to x86_64-linux.
113 '';
114 };
115
116 configFile = mkOption {
117 type = types.nullOr types.path;
118 description = ''
119 The path to the default configuration options the PulseAudio server
120 should use. By default, the "default.pa" configuration
121 from the PulseAudio distribution is used.
122 '';
123 };
124
125 extraConfig = mkOption {
126 type = types.lines;
127 default = "";
128 description = ''
129 Literal string to append to <literal>configFile</literal>
130 and the config file generated by the pulseaudio module.
131 '';
132 };
133
134 extraClientConf = mkOption {
135 type = types.lines;
136 default = "";
137 description = ''
138 Extra configuration appended to pulse/client.conf file.
139 '';
140 };
141
142 package = mkOption {
143 type = types.package;
144 default = pulseaudioLight;
145 defaultText = "pkgs.pulseaudioLight";
146 example = literalExample "pkgs.pulseaudioFull";
147 description = ''
148 The PulseAudio derivation to use. This can be used to enable
149 features (such as JACK support, Bluetooth) via the
150 <literal>pulseaudioFull</literal> package.
151 '';
152 };
153
154 daemon = {
155 logLevel = mkOption {
156 type = types.str;
157 default = "notice";
158 description = ''
159 The log level that the system-wide pulseaudio daemon should use,
160 if activated.
161 '';
162 };
163 };
164
165 zeroconf = {
166 discovery.enable =
167 mkEnableOption "discovery of pulseaudio sinks in the local network";
168 publish.enable =
169 mkEnableOption "publishing the pulseaudio sink in the local network";
170 };
171
172 # TODO: enable by default?
173 tcp = {
174 enable = mkEnableOption "tcp streaming support";
175
176 anonymousClients = {
177 allowAll = mkEnableOption "all anonymous clients to stream to the server";
178 allowedIpRanges = mkOption {
179 type = types.listOf types.str;
180 default = [];
181 example = literalExample ''[ "127.0.0.1" "192.168.1.0/24" ]'';
182 description = ''
183 A list of IP subnets that are allowed to stream to the server.
184 '';
185 };
186 };
187 };
188
189 };
190
191 };
192
193
194 config = mkMerge [
195 {
196 environment.etc = singleton {
197 target = "pulse/client.conf";
198 source = clientConf;
199 };
200
201 hardware.pulseaudio.configFile = mkDefault "${getBin overriddenPackage}/etc/pulse/default.pa";
202 }
203
204 (mkIf cfg.enable {
205 environment.systemPackages = [ overriddenPackage ];
206
207 environment.etc = singleton {
208 target = "asound.conf";
209 source = alsaConf;
210 };
211
212 # Allow PulseAudio to get realtime priority using rtkit.
213 security.rtkit.enable = true;
214
215 })
216
217 (mkIf hasZeroconf {
218 services.avahi.enable = true;
219 })
220 (mkIf cfg.zeroconf.publish.enable {
221 services.avahi.publish.enable = true;
222 services.avahi.publish.userServices = true;
223 })
224
225 (mkIf nonSystemWide {
226 environment.etc = singleton {
227 target = "pulse/default.pa";
228 source = myConfigFile;
229 };
230
231 systemd.user = {
232 services.pulseaudio = {
233 description = "PulseAudio Server";
234 # NixOS doesn't support "Also" so we bring it in manually
235 wantedBy = [ "default.target" ];
236 serviceConfig = {
237 Type = "notify";
238 ExecStart = binaryNoDaemon;
239 Restart = "on-failure";
240 RestartSec = "500ms";
241 };
242 environment = { DISPLAY = ":${toString config.services.xserver.display}"; };
243 restartIfChanged = true;
244 };
245
246 sockets.pulseaudio = {
247 description = "PulseAudio Socket";
248 wantedBy = [ "sockets.target" ];
249 socketConfig = {
250 Priority = 6;
251 Backlog = 5;
252 ListenStream = "%t/pulse/native";
253 };
254 };
255 };
256 })
257
258 (mkIf systemWide {
259 users.extraUsers.pulse = {
260 # For some reason, PulseAudio wants UID == GID.
261 uid = assert uid == gid; uid;
262 group = "pulse";
263 extraGroups = [ "audio" ];
264 description = "PulseAudio system service user";
265 home = stateDir;
266 createHome = true;
267 };
268
269 users.extraGroups.pulse.gid = gid;
270
271 systemd.services.pulseaudio = {
272 description = "PulseAudio System-Wide Server";
273 wantedBy = [ "sound.target" ];
274 before = [ "sound.target" ];
275 environment.PULSE_RUNTIME_PATH = stateDir;
276 serviceConfig = {
277 Type = "notify";
278 ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}";
279 Restart = "on-failure";
280 RestartSec = "500ms";
281 };
282 };
283 })
284 ];
285
286}