nixos/alsa: rebirth from the ashes (#337345)
This commit is contained in:
@@ -936,15 +936,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
|
||||
|
||||
@@ -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=<name>`
|
||||
}
|
||||
'';
|
||||
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,18 @@
|
||||
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";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
meta.maintainers = with lib.maintainers; [ rnhmjoj ];
|
||||
|
||||
}
|
||||
|
||||
+6
-28
@@ -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";
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user