at 25.11-pre 14 kB view raw
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}