From c8a84a01d57ea72a5e560f69cdd8596b7f7421b2 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Sun, 25 Aug 2024 19:16:24 +0200 Subject: [PATCH 1/3] nixos/alsa: rebirth from the ashes The ALSA module was essentially removed in 3eeff547, with the main motivation of avoiding confusion as to what `sound.enable` really meant. As that could be achieved with a simple rename, this change brings back the module in full force under the `hardware.alsa` namespace (with clear beware signs for the pulse and pipewire folks) and adds a lot of useful extra features. These include - `defaultDevice` to set the default playback and capture devices - `cardAliases`,`deviceAliases` to assign meaningful names to sound cards and devices (instead of say, `hw:0,1`) - `controls` to create virtual volume controls - `enableRecorder` to easily configure a loopback device to record the computer audio - fixes to the udev restore rules --- .../manual/release-notes/rl-2411.section.md | 9 +- nixos/modules/services/audio/alsa.nix | 431 +++++++++++++++++- 2 files changed, 420 insertions(+), 20 deletions(-) diff --git a/nixos/doc/manual/release-notes/rl-2411.section.md b/nixos/doc/manual/release-notes/rl-2411.section.md index 84c2f6fe344a..7d7c8f501167 100644 --- a/nixos/doc/manual/release-notes/rl-2411.section.md +++ b/nixos/doc/manual/release-notes/rl-2411.section.md @@ -919,15 +919,16 @@ The `sound` options have been largely removed, as they are unnecessary for most If you set `sound.enable` in your configuration: - If you are using Pulseaudio or PipeWire, simply remove that option - - If you are not using an external sound server, and want volumes to be persisted across shutdowns, set `hardware.alsa.enablePersistence = true` instead + - If you are using ALSA as your only sound system (no sound server), set `hardware.alsa.enable = true` instead If you set `sound.enableOSSEmulation` in your configuration: - Make sure it is still necessary, as very few applications actually use OSS - - If necessary, set `boot.kernelModules = [ "snd_pcm_oss" ]` + - If necessary, set `hardware.alsa.enableOSSEmulation = true` If you set `sound.extraConfig` in your configuration: - - If you are using another sound server, like Pulseaudio, JACK or PipeWire, migrate your configuration to that - - If you are not using an external sound server, set `environment.etc."asound.conf".text = yourExtraConfig` instead + - If you are using a sound server, like Pulseaudio, JACK or PipeWire, migrate your configuration to that + - If you are using ALSA as your only sound system, check if you can use the new structured ALSA options `hardware.alsa.defaultDevice`, `hardware.alsa.cardAliases`, `hardware.alsa.controls`, etc. + - Otherwise, move your configuration directly into `hardware.alsa.config` If you set `sound.mediaKeys` in your configuration: - Preferably switch to handling media keys in your desktop environment/compositor diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix index fac0012b666d..a40b1c49cd89 100644 --- a/nixos/modules/services/audio/alsa.nix +++ b/nixos/modules/services/audio/alsa.nix @@ -1,27 +1,420 @@ -# ALSA sound support. -{ config, lib, pkgs, ... }: +{ + config, + lib, + pkgs, + ... +}: + +let + cfg = config.hardware.alsa; + + quote = x: ''"${lib.escape [ "\"" ] x}"''; + + alsactl = lib.getExe' pkgs.alsa-utils "alsactl"; + + # Creates a volume control + mkControl = name: opts: '' + pcm.${name} { + type softvol + slave.pcm ${quote opts.device} + control.name ${quote (if opts.name != null then opts.name else name)} + control.card ${quote opts.card} + max_dB ${toString opts.maxVolume} + } + ''; + + # modprobe.conf for naming sound cards + cardsConfig = + let + # Reverse the mapping from card name→driver to card driver→name + drivers = lib.unique (lib.mapAttrsToList (n: v: v.driver) cfg.cardAliases); + options = lib.forEach drivers ( + drv: + let + byDriver = lib.filterAttrs (n: v: v.driver == drv); + ids = lib.mapAttrs (n: v: v.id) (byDriver cfg.cardAliases); + in + { + driver = drv; + names = lib.attrNames ids; + ids = lib.attrValues ids; + } + ); + toList = x: lib.concatStringsSep "," (map toString x); + in + lib.forEach options (i: "options ${i.driver} index=${toList i.ids} id=${toList i.names}"); + + defaultDeviceVars = { + "ALSA_AUDIO_OUT" = cfg.defaultDevice.playback; + "ALSA_AUDIO_IN" = cfg.defaultDevice.capture; + }; + +in { imports = [ - (lib.mkRemovedOptionModule [ "sound" ] "The option was heavily overloaded and can be removed from most configurations.") + (lib.mkRemovedOptionModule [ "sound" "enable" ] '' + The option was heavily overloaded and can be removed from most configurations. + To specifically configure the user space part of ALSA, see `hardware.alsa`. + '') + (lib.mkRemovedOptionModule [ "sound" "mediaKeys" ] '' + The media keys can be configured with any hotkey daemon (that better + integrates with your desktop setup). To continue using the actkbd daemon + (which was used up to NixOS 24.05), add these lines to your configuration: + + services.actkbd.enable = true; + services.actkbd.bindings = [ + # Mute + { keys = [ 113 ]; events = [ "key" ]; + command = "''${pkgs.alsa-utils}/bin/amixer -q set Master toggle"; + } + # Volume down + { keys = [ 114 ]; events = [ "key" "rep" ]; + command = "''${pkgs.alsa-utils}/bin/amixer -q set Master 1- unmute"; + } + # Volume up + { keys = [ 115 ]; events = [ "key" "rep" ]; + command = "''${pkgs.alsa-utils}/bin/amixer -q set Master 1+ unmute"; + } + # Mic Mute + { keys = [ 190 ]; events = [ "key" ]; + command = "''${pkgs.alsa-utils}/bin/amixer -q set Capture toggle"; + } + ]; + '') + (lib.mkRenamedOptionModule + [ "sound" "enableOSSEmulation" ] + [ "hardware" "alsa" "enableOSSEmulation" ] + ) + (lib.mkRenamedOptionModule + [ "sound" "extraConfig" ] + [ "hardware" "alsa" "config" ]) ]; - options.hardware.alsa.enablePersistence = lib.mkOption { - type = lib.types.bool; - default = false; - description = '' - Whether to enable ALSA sound card state saving on shutdown. - This is generally not necessary if you're using an external sound server. - ''; + options.hardware.alsa = { + + enable = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to set up the user space part of the Advanced Linux Sound Architecture (ALSA) + + ::: {.warning} + Enable this option only if you want to use ALSA as your main sound system, + not if you're using a sound server (e.g. PulseAudio or Pipewire). + ::: + ''; + }; + + enableOSSEmulation = lib.mkEnableOption "the OSS emulation"; + + enableRecorder = lib.mkOption { + type = lib.types.bool; + default = false; + description = '' + Whether to set up a loopback device that continuously records and + allows to play back audio from the computer. + + The loopback device is named `pcm.recorder`, audio can be saved + by capturing from this device as with any microphone. + + ::: {.note} + By default the output is duplicated to the recorder assuming stereo + audio, for a more complex layout you have to override the pcm.splitter + device using `hardware.alsa.config`. + See the generated /etc/asound.conf for its definition. + ::: + ''; + }; + + defaultDevice.playback = lib.mkOption { + type = lib.types.str; + default = ""; + example = "dmix:CARD=1,DEV=0"; + description = '' + The default playback device. + Leave empty to let ALSA pick the default automatically. + + ::: {.note} + The device can be changed at runtime by setting the ALSA_AUDIO_OUT + environment variables (but only before starting a program). + ::: + ''; + }; + + defaultDevice.capture = lib.mkOption { + type = lib.types.str; + default = ""; + example = "dsnoop:CARD=0,DEV=2"; + description = '' + The default capture device (i.e. microphone). + Leave empty to let ALSA pick the default automatically. + + ::: {.note} + The device can be changed at runtime by setting the ALSA_AUDIO_IN + environment variables (but only before starting a program). + ::: + ''; + }; + + controls = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ({ + options.name = lib.mkOption { + type = lib.types.nullOr lib.types.str; + default = null; + description = '' + Name of the control, as it appears in `alsamixer`. + If null it will be the same as the softvol device name. + ''; + }; + options.device = lib.mkOption { + type = lib.types.str; + default = "default"; + description = '' + Name of the PCM device to control (slave). + ''; + }; + options.card = lib.mkOption { + type = lib.types.str; + default = "default"; + description = '' + Name of the PCM card to control (slave). + ''; + }; + options.maxVolume = lib.mkOption { + type = lib.types.float; + default = 0.0; + description = '' + The maximum volume in dB. + ''; + }; + }) + ); + default = {}; + example = lib.literalExpression '' + { + firefox = { device = "front"; maxVolume = -25.0; }; + mpv = { device = "front"; maxVolume = -25.0; }; + # and run programs with `env ALSA_AUDIO_OUT=` + } + ''; + description = '' + Virtual volume controls (softvols) to add to a sound card. + These can be used to control the volume of specific applications + or a digital output device (HDMI video card). + ''; + }; + + cardAliases = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule ({ + options.driver = lib.mkOption { + type = lib.types.str; + description = '' + Name of the kernel module that provides the card. + ''; + }; + options.id = lib.mkOption { + type = lib.types.int; + default = "default"; + description = '' + The ID of the sound card + ''; + }; + }) + ); + default = { }; + example = lib.literalExpression '' + { + soundchip = { driver = "snd_intel_hda"; id = 0; }; + videocard = { driver = "snd_intel_hda"; id = 1; }; + usb = { driver = "snd_usb_audio"; id = 2; }; + } + ''; + description = '' + Assign custom names and reorder the sound cards. + + ::: {.note} + You can find the card ids by looking at `/proc/asound/cards`. + ::: + ''; + }; + + deviceAliases = lib.mkOption { + type = lib.types.attrsOf lib.types.str; + default = { }; + example = lib.literalExpression '' + { + hdmi1 = "hw:CARD=videocard,DEV=5"; + hdmi2 = "hw:CARD=videocard,DEV=6"; + } + ''; + description = '' + Assign custom names to sound cards. + ''; + }; + + config = lib.mkOption { + type = lib.types.lines; + default = ""; + example = lib.literalExpression '' + # Send audio to a remote host via SSH + pcm.remote { + @args [ HOSTNAME ] + @args.HOSTNAME { type string } + type file + format raw + slave.pcm pcm.null + file { + @func concat + strings [ + "| ''${lib.getExec pkgs.openssh} -C " + $HOSTNAME + " aplay -f %f -c %c -r %r -" + ] + } + } + ''; + description = '' + The content of the system-wide ALSA configuration (/etc/asound.conf). + + Documentation of the configuration language and examples can be found + in the unofficial ALSA wiki: https://alsa.opensrc.org/Asoundrc + ''; + }; + }; - config = lib.mkIf config.hardware.alsa.enablePersistence { - # ALSA provides a udev rule for restoring volume settings. - services.udev.packages = [ pkgs.alsa-utils ]; + config = lib.mkIf cfg.enable { + # Disable sound servers enabled by default and, + # if the user enabled one manually, cause a conflict. + services.pipewire.enable = false; + hardware.pulseaudio.enable = false; + + hardware.alsa.config = + let + conf = [ + '' + pcm.!default fromenv + + # Read the capture and playback device from + # the ALSA_AUDIO_IN, ALSA_AUDIO_OUT variables + pcm.fromenv { + type asym + playback.pcm { + type plug + slave.pcm { + @func getenv + vars [ ALSA_AUDIO_OUT ] + default pcm.sysdefault + } + } + capture.pcm { + type plug + slave.pcm { + @func getenv + vars [ ALSA_AUDIO_IN ] + default pcm.sysdefault + } + } + } + '' + (lib.optional cfg.enableRecorder '' + pcm.!default "splitter:fromenv,recorder" + + # Send audio to two stereo devices + pcm.splitter { + @args [ A B ] + @args.A.type string + @args.B.type string + type asym + playback.pcm { + type plug + route_policy "duplicate" + slave.pcm { + type multi + slaves.a.pcm $A + slaves.b.pcm $B + slaves.a.channels 2 + slaves.b.channels 2 + bindings [ + { slave a channel 0 } + { slave a channel 1 } + { slave b channel 0 } + { slave b channel 1 } + ] + } + } + capture.pcm $A + } + + # Device which records and plays back audio + pcm.recorder { + type asym + capture.pcm { + type dsnoop + ipc_key 9165218 + ipc_perm 0666 + slave.pcm "hw:loopback,1,0" + slave.period_size 1024 + slave.buffer_size 8192 + } + playback.pcm { + type dmix + ipc_key 6181923 + ipc_perm 0666 + slave.pcm "hw:loopback,0,0" + slave.period_size 1024 + slave.buffer_size 8192 + } + } + '') + (lib.mapAttrsToList mkControl cfg.controls) + (lib.mapAttrsToList (n: v: "pcm.${n} ${quote v}") cfg.deviceAliases) + ]; + in + lib.mkBefore (lib.concatStringsSep "\n" (lib.flatten conf)); + + hardware.alsa.cardAliases = lib.mkIf cfg.enableRecorder { + loopback.driver = "snd_aloop"; + loopback.id = 2; + }; + + # Set default PCM devices + environment.sessionVariables = defaultDeviceVars; + systemd.globalEnvironment = defaultDeviceVars; + + environment.etc."asound.conf".text = cfg.config; + + boot.kernelModules = + [ ] + ++ lib.optionals cfg.enableOSSEmulation [ "snd_pcm_oss" "snd_mixer_oss" ] + ++ lib.optionals cfg.enableRecorder [ "snd_aloop" ]; + + # Assign names to the sound cards + boot.extraModprobeConfig = lib.concatStringsSep "\n" cardsConfig; + + # Provide alsamixer, aplay, arecord, etc. + environment.systemPackages = [ pkgs.alsa-utils ]; + + # Install udev rules for restoring card settings on boot + services.udev.extraRules = '' + ACTION=="add", SUBSYSTEM=="sound", KERNEL=="controlC*", KERNELS!="card*", GOTO="alsa_restore_go" + GOTO="alsa_restore_end" + + LABEL="alsa_restore_go" + TEST!="/etc/alsa/state-daemon.conf", RUN+="${alsactl} restore -gU $attr{device/number}" + TEST=="/etc/alsa/state-daemon.conf", RUN+="${alsactl} nrestore -gU $attr{device/number}" + LABEL="alsa_restore_end" + ''; + + # Service to store/restore the sound card settings systemd.services.alsa-store = { description = "Store Sound Card State"; wantedBy = [ "multi-user.target" ]; + restartIfChanged = false; unitConfig = { RequiresMountsFor = "/var/lib/alsa"; ConditionVirtualization = "!systemd-nspawn"; @@ -29,10 +422,16 @@ serviceConfig = { Type = "oneshot"; RemainAfterExit = true; - ExecStartPre = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa"; - ExecStart = "${pkgs.alsa-utils}/sbin/alsactl restore --ignore"; - ExecStop = "${pkgs.alsa-utils}/sbin/alsactl store --ignore"; + StateDirectory = "alsa"; + # Note: the service should never be restated, otherwise any + # setting changed between the last `store` and now will be lost. + # To prevent NixOS from starting it in case it has failed we + # expand the exit codes considered successful + SuccessExitStatus = [ 0 99 ]; + ExecStart = "${alsactl} restore -gU"; + ExecStop = "${alsactl} store -gU"; }; }; }; + } From 0b5ca42f3136c25c90bd27dc65aa9fdf941ee462 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Mon, 9 Dec 2024 21:35:44 +0100 Subject: [PATCH 2/3] nixos/tests/firefox: use hardware.alsa --- nixos/tests/firefox.nix | 34 ++++++---------------------------- 1 file changed, 6 insertions(+), 28 deletions(-) diff --git a/nixos/tests/firefox.nix b/nixos/tests/firefox.nix index 8243defbb9f2..9388af953a35 100644 --- a/nixos/tests/firefox.nix +++ b/nixos/tests/firefox.nix @@ -18,37 +18,15 @@ import ./make-test-python.nix ({ lib, pkgs, firefoxPackage, ... }: package = firefoxPackage; }; - # Create a virtual sound device, with mixing - # and all, for recording audio. - boot.kernelModules = [ "snd-aloop" ]; - environment.etc."asound.conf".text = '' - pcm.!default { - type plug - slave.pcm pcm.dmixer - } - pcm.dmixer { - type dmix - ipc_key 1 - slave { - pcm "hw:Loopback,0,0" - rate 48000 - periods 128 - period_time 0 - period_size 1024 - buffer_size 8192 - } - } - pcm.recorder { - type hw - card "Loopback" - device 1 - subdevice 0 - } - ''; + hardware.alsa = { + enable = true; + enableRecorder = true; + defaultDevice.playback = "pcm.recorder"; + }; systemd.services.audio-recorder = { description = "Record NixOS test audio to /tmp/record.wav"; - script = "${pkgs.alsa-utils}/bin/arecord -D recorder -f S16_LE -r48000 /tmp/record.wav"; + script = "${pkgs.alsa-utils}/bin/arecord -Drecorder -fS16_LE -r48000 -c2 /tmp/record.wav"; }; }; From b0ee1095295db1defd7f493f14697c77d17d3088 Mon Sep 17 00:00:00 2001 From: rnhmjoj Date: Tue, 10 Dec 2024 11:46:49 +0100 Subject: [PATCH 3/3] nixos/alsa: add rnhmjoj as maintainer --- nixos/modules/services/audio/alsa.nix | 2 ++ 1 file changed, 2 insertions(+) diff --git a/nixos/modules/services/audio/alsa.nix b/nixos/modules/services/audio/alsa.nix index a40b1c49cd89..a26dc493cdb0 100644 --- a/nixos/modules/services/audio/alsa.nix +++ b/nixos/modules/services/audio/alsa.nix @@ -434,4 +434,6 @@ in }; }; + meta.maintainers = with lib.maintainers; [ rnhmjoj ]; + }