1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7
8let
9 cfg = config.hardware.alsa;
10
11 quote = x: ''"${lib.escape [ "\"" ] x}"'';
12
13 alsactl = lib.getExe' pkgs.alsa-utils "alsactl";
14
15 # Creates a volume control
16 mkControl = name: opts: ''
17 pcm.${name} {
18 type softvol
19 slave.pcm ${quote opts.device}
20 control.name ${quote (if opts.name != null then opts.name else name)}
21 control.card ${quote opts.card}
22 max_dB ${toString opts.maxVolume}
23 }
24 '';
25
26 # modprobe.conf for naming sound cards
27 cardsConfig =
28 let
29 # Reverse the mapping from card name→driver to card driver→name
30 drivers = lib.unique (lib.mapAttrsToList (n: v: v.driver) cfg.cardAliases);
31 options = lib.forEach drivers (
32 drv:
33 let
34 byDriver = lib.filterAttrs (n: v: v.driver == drv);
35 ids = lib.mapAttrs (n: v: v.id) (byDriver cfg.cardAliases);
36 in
37 {
38 driver = drv;
39 names = lib.attrNames ids;
40 ids = lib.attrValues ids;
41 }
42 );
43 toList = x: lib.concatStringsSep "," (map toString x);
44 in
45 lib.forEach options (i: "options ${i.driver} index=${toList i.ids} id=${toList i.names}");
46
47 defaultDeviceVars = {
48 "ALSA_AUDIO_OUT" = cfg.defaultDevice.playback;
49 "ALSA_AUDIO_IN" = cfg.defaultDevice.capture;
50 };
51
52in
53
54{
55 imports = [
56 (lib.mkRemovedOptionModule [ "sound" "enable" ] ''
57 The option was heavily overloaded and can be removed from most configurations.
58 To specifically configure the user space part of ALSA, see `hardware.alsa`.
59 '')
60 (lib.mkRemovedOptionModule [ "sound" "mediaKeys" ] ''
61 The media keys can be configured with any hotkey daemon (that better
62 integrates with your desktop setup). To continue using the actkbd daemon
63 (which was used up to NixOS 24.05), add these lines to your configuration:
64
65 services.actkbd.enable = true;
66 services.actkbd.bindings = [
67 # Mute
68 { keys = [ 113 ]; events = [ "key" ];
69 command = "''${pkgs.alsa-utils}/bin/amixer -q set Master toggle";
70 }
71 # Volume down
72 { keys = [ 114 ]; events = [ "key" "rep" ];
73 command = "''${pkgs.alsa-utils}/bin/amixer -q set Master 1- unmute";
74 }
75 # Volume up
76 { keys = [ 115 ]; events = [ "key" "rep" ];
77 command = "''${pkgs.alsa-utils}/bin/amixer -q set Master 1+ unmute";
78 }
79 # Mic Mute
80 { keys = [ 190 ]; events = [ "key" ];
81 command = "''${pkgs.alsa-utils}/bin/amixer -q set Capture toggle";
82 }
83 ];
84 '')
85 (lib.mkRenamedOptionModule
86 [ "sound" "enableOSSEmulation" ]
87 [ "hardware" "alsa" "enableOSSEmulation" ]
88 )
89 (lib.mkRenamedOptionModule [ "sound" "extraConfig" ] [ "hardware" "alsa" "config" ])
90 ];
91
92 options.hardware.alsa = {
93
94 enable = lib.mkOption {
95 type = lib.types.bool;
96 default = false;
97 description = ''
98 Whether to set up the user space part of the Advanced Linux Sound Architecture (ALSA)
99
100 ::: {.warning}
101 Enable this option only if you want to use ALSA as your main sound system,
102 not if you're using a sound server (e.g. PulseAudio or Pipewire).
103 :::
104 '';
105 };
106
107 enableOSSEmulation = lib.mkEnableOption "the OSS emulation";
108
109 enableRecorder = lib.mkOption {
110 type = lib.types.bool;
111 default = false;
112 description = ''
113 Whether to set up a loopback device that continuously records and
114 allows to play back audio from the computer.
115
116 The loopback device is named `pcm.recorder`, audio can be saved
117 by capturing from this device as with any microphone.
118
119 ::: {.note}
120 By default the output is duplicated to the recorder assuming stereo
121 audio, for a more complex layout you have to override the pcm.splitter
122 device using `hardware.alsa.config`.
123 See the generated /etc/asound.conf for its definition.
124 :::
125 '';
126 };
127
128 defaultDevice.playback = lib.mkOption {
129 type = lib.types.str;
130 default = "";
131 example = "dmix:CARD=1,DEV=0";
132 description = ''
133 The default playback device.
134 Leave empty to let ALSA pick the default automatically.
135
136 ::: {.note}
137 The device can be changed at runtime by setting the ALSA_AUDIO_OUT
138 environment variables (but only before starting a program).
139 :::
140 '';
141 };
142
143 defaultDevice.capture = lib.mkOption {
144 type = lib.types.str;
145 default = "";
146 example = "dsnoop:CARD=0,DEV=2";
147 description = ''
148 The default capture device (i.e. microphone).
149 Leave empty to let ALSA pick the default automatically.
150
151 ::: {.note}
152 The device can be changed at runtime by setting the ALSA_AUDIO_IN
153 environment variables (but only before starting a program).
154 :::
155 '';
156 };
157
158 controls = lib.mkOption {
159 type = lib.types.attrsOf (
160 lib.types.submodule ({
161 options.name = lib.mkOption {
162 type = lib.types.nullOr lib.types.str;
163 default = null;
164 description = ''
165 Name of the control, as it appears in `alsamixer`.
166 If null it will be the same as the softvol device name.
167 '';
168 };
169 options.device = lib.mkOption {
170 type = lib.types.str;
171 default = "default";
172 description = ''
173 Name of the PCM device to control (slave).
174 '';
175 };
176 options.card = lib.mkOption {
177 type = lib.types.str;
178 default = "default";
179 description = ''
180 Name of the PCM card to control (slave).
181 '';
182 };
183 options.maxVolume = lib.mkOption {
184 type = lib.types.float;
185 default = 0.0;
186 description = ''
187 The maximum volume in dB.
188 '';
189 };
190 })
191 );
192 default = { };
193 example = lib.literalExpression ''
194 {
195 firefox = { device = "front"; maxVolume = -25.0; };
196 mpv = { device = "front"; maxVolume = -25.0; };
197 # and run programs with `env ALSA_AUDIO_OUT=<name>`
198 }
199 '';
200 description = ''
201 Virtual volume controls (softvols) to add to a sound card.
202 These can be used to control the volume of specific applications
203 or a digital output device (HDMI video card).
204 '';
205 };
206
207 cardAliases = lib.mkOption {
208 type = lib.types.attrsOf (
209 lib.types.submodule ({
210 options.driver = lib.mkOption {
211 type = lib.types.str;
212 description = ''
213 Name of the kernel module that provides the card.
214 '';
215 };
216 options.id = lib.mkOption {
217 type = lib.types.int;
218 default = "default";
219 description = ''
220 The ID of the sound card
221 '';
222 };
223 })
224 );
225 default = { };
226 example = lib.literalExpression ''
227 {
228 soundchip = { driver = "snd_intel_hda"; id = 0; };
229 videocard = { driver = "snd_intel_hda"; id = 1; };
230 usb = { driver = "snd_usb_audio"; id = 2; };
231 }
232 '';
233 description = ''
234 Assign custom names and reorder the sound cards.
235
236 ::: {.note}
237 You can find the card ids by looking at `/proc/asound/cards`.
238 :::
239 '';
240 };
241
242 deviceAliases = lib.mkOption {
243 type = lib.types.attrsOf lib.types.str;
244 default = { };
245 example = lib.literalExpression ''
246 {
247 hdmi1 = "hw:CARD=videocard,DEV=5";
248 hdmi2 = "hw:CARD=videocard,DEV=6";
249 }
250 '';
251 description = ''
252 Assign custom names to sound cards.
253 '';
254 };
255
256 config = lib.mkOption {
257 type = lib.types.lines;
258 default = "";
259 example = lib.literalExpression ''
260 # Send audio to a remote host via SSH
261 pcm.remote {
262 @args [ HOSTNAME ]
263 @args.HOSTNAME { type string }
264 type file
265 format raw
266 slave.pcm pcm.null
267 file {
268 @func concat
269 strings [
270 "| ''${lib.getExec pkgs.openssh} -C "
271 $HOSTNAME
272 " aplay -f %f -c %c -r %r -"
273 ]
274 }
275 }
276 '';
277 description = ''
278 The content of the system-wide ALSA configuration (/etc/asound.conf).
279
280 Documentation of the configuration language and examples can be found
281 in the unofficial ALSA wiki: https://alsa.opensrc.org/Asoundrc
282 '';
283 };
284
285 };
286
287 options.hardware.alsa.enablePersistence = lib.mkOption {
288 type = lib.types.bool;
289 defaultText = lib.literalExpression "config.hardware.alsa.enable";
290 default = config.hardware.alsa.enable;
291 description = ''
292 Whether to enable ALSA sound card state saving on shutdown.
293 This is generally not necessary if you're using an external sound server.
294 '';
295 };
296
297 config = lib.mkMerge [
298
299 (lib.mkIf cfg.enable {
300 # Disable sound servers enabled by default and,
301 # if the user enabled one manually, cause a conflict.
302 services.pipewire.enable = false;
303 services.pulseaudio.enable = false;
304
305 hardware.alsa.config =
306 let
307 conf = [
308 ''
309 pcm.!default fromenv
310
311 # Read the capture and playback device from
312 # the ALSA_AUDIO_IN, ALSA_AUDIO_OUT variables
313 pcm.fromenv {
314 type asym
315 playback.pcm {
316 type plug
317 slave.pcm {
318 @func getenv
319 vars [ ALSA_AUDIO_OUT ]
320 default pcm.sysdefault
321 }
322 }
323 capture.pcm {
324 type plug
325 slave.pcm {
326 @func getenv
327 vars [ ALSA_AUDIO_IN ]
328 default pcm.sysdefault
329 }
330 }
331 }
332 ''
333 (lib.optional cfg.enableRecorder ''
334 pcm.!default "splitter:fromenv,recorder"
335
336 # Send audio to two stereo devices
337 pcm.splitter {
338 @args [ A B ]
339 @args.A.type string
340 @args.B.type string
341 type asym
342 playback.pcm {
343 type plug
344 route_policy "duplicate"
345 slave.pcm {
346 type multi
347 slaves.a.pcm $A
348 slaves.b.pcm $B
349 slaves.a.channels 2
350 slaves.b.channels 2
351 bindings [
352 { slave a channel 0 }
353 { slave a channel 1 }
354 { slave b channel 0 }
355 { slave b channel 1 }
356 ]
357 }
358 }
359 capture.pcm $A
360 }
361
362 # Device which records and plays back audio
363 pcm.recorder {
364 type asym
365 capture.pcm {
366 type dsnoop
367 ipc_key 9165218
368 ipc_perm 0666
369 slave.pcm "hw:loopback,1,0"
370 slave.period_size 1024
371 slave.buffer_size 8192
372 }
373 playback.pcm {
374 type dmix
375 ipc_key 6181923
376 ipc_perm 0666
377 slave.pcm "hw:loopback,0,0"
378 slave.period_size 1024
379 slave.buffer_size 8192
380 }
381 }
382 '')
383 (lib.mapAttrsToList mkControl cfg.controls)
384 (lib.mapAttrsToList (n: v: "pcm.${n} ${quote v}") cfg.deviceAliases)
385 ];
386 in
387 lib.mkBefore (lib.concatStringsSep "\n" (lib.flatten conf));
388
389 hardware.alsa.cardAliases = lib.mkIf cfg.enableRecorder {
390 loopback.driver = "snd_aloop";
391 loopback.id = 2;
392 };
393
394 # Set default PCM devices
395 environment.sessionVariables = defaultDeviceVars;
396 systemd.globalEnvironment = defaultDeviceVars;
397
398 environment.etc."asound.conf".text = cfg.config;
399
400 boot.kernelModules =
401 [ ]
402 ++ lib.optionals cfg.enableOSSEmulation [
403 "snd_pcm_oss"
404 "snd_mixer_oss"
405 ]
406 ++ lib.optionals cfg.enableRecorder [ "snd_aloop" ];
407
408 # Assign names to the sound cards
409 boot.extraModprobeConfig = lib.concatStringsSep "\n" cardsConfig;
410
411 # Provide alsamixer, aplay, arecord, etc.
412 environment.systemPackages = [ pkgs.alsa-utils ];
413 })
414
415 (lib.mkIf config.hardware.alsa.enablePersistence {
416
417 # Install udev rules for restoring card settings on boot
418 services.udev.extraRules = ''
419 ACTION=="add", SUBSYSTEM=="sound", KERNEL=="controlC*", KERNELS!="card*", GOTO="alsa_restore_go"
420 GOTO="alsa_restore_end"
421
422 LABEL="alsa_restore_go"
423 TEST!="/etc/alsa/state-daemon.conf", RUN+="${alsactl} restore -gU $attr{device/number}"
424 TEST=="/etc/alsa/state-daemon.conf", RUN+="${alsactl} nrestore -gU $attr{device/number}"
425 LABEL="alsa_restore_end"
426 '';
427
428 # Service to store/restore the sound card settings
429 systemd.services.alsa-store = {
430 description = "Store Sound Card State";
431 wantedBy = [ "multi-user.target" ];
432 restartIfChanged = false;
433 unitConfig = {
434 RequiresMountsFor = "/var/lib/alsa";
435 ConditionVirtualization = "!systemd-nspawn";
436 };
437 serviceConfig = {
438 Type = "oneshot";
439 RemainAfterExit = true;
440 StateDirectory = "alsa";
441 # Note: the service should never be restated, otherwise any
442 # setting changed between the last `store` and now will be lost.
443 # To prevent NixOS from starting it in case it has failed we
444 # expand the exit codes considered successful
445 SuccessExitStatus = [
446 0
447 99
448 ];
449 ExecStart = "${alsactl} restore -gU";
450 ExecStop = "${alsactl} store -gU";
451 };
452 };
453 })
454
455 ];
456
457 meta.maintainers = with lib.maintainers; [ rnhmjoj ];
458
459}