{ config, lib, pkgs, ... }: let cfg = config.hardware.pulseaudio; hasZeroconf = let z = cfg.zeroconf; in z.publish.enable || z.discovery.enable; overriddenPackage = cfg.package.override (lib.optionalAttrs hasZeroconf { zeroconfSupport = true; }); binary = "${lib.getBin overriddenPackage}/bin/pulseaudio"; binaryNoDaemon = "${binary} --daemonize=no"; # Forces 32bit pulseaudio and alsa-plugins to be built/supported for apps # using 32bit alsa on 64bit linux. enable32BitAlsaPlugins = cfg.support32Bit && pkgs.stdenv.isx86_64 && (pkgs.pkgsi686Linux.alsa-lib != null && pkgs.pkgsi686Linux.libpulseaudio != null); myConfigFile = let addModuleIf = cond: mod: lib.optionalString cond "load-module ${mod}"; allAnon = lib.optional cfg.tcp.anonymousClients.allowAll "auth-anonymous=1"; ipAnon = let a = cfg.tcp.anonymousClients.allowedIpRanges; in lib.optional (a != []) ''auth-ip-acl=${lib.concatStringsSep ";" a}''; in pkgs.writeTextFile { name = "default.pa"; text = '' .include ${cfg.configFile} ${addModuleIf cfg.zeroconf.publish.enable "module-zeroconf-publish"} ${addModuleIf cfg.zeroconf.discovery.enable "module-zeroconf-discover"} ${addModuleIf cfg.tcp.enable (lib.concatStringsSep " " ([ "module-native-protocol-tcp" ] ++ allAnon ++ ipAnon))} ${addModuleIf config.services.jack.jackd.enable "module-jack-sink"} ${addModuleIf config.services.jack.jackd.enable "module-jack-source"} ${cfg.extraConfig} ''; }; ids = config.ids; uid = ids.uids.pulseaudio; gid = ids.gids.pulseaudio; stateDir = "/run/pulse"; # Create pulse/client.conf even if PulseAudio is disabled so # that we can disable the autospawn feature in programs that # are built with PulseAudio support (like KDE). clientConf = pkgs.writeText "client.conf" '' autospawn=no ${cfg.extraClientConf} ''; # Write an /etc/asound.conf that causes all ALSA applications to # be re-routed to the PulseAudio server through ALSA's Pulse # plugin. alsaConf = '' pcm_type.pulse { libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ; ${lib.optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_pulse.so ;"} } pcm.!default { type pulse hint.description "Default Audio Device (via PulseAudio)" } ctl_type.pulse { libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ; ${lib.optionalString enable32BitAlsaPlugins "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_ctl_pulse.so ;"} } ctl.!default { type pulse } ''; in { options = { hardware.pulseaudio = { enable = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to enable the PulseAudio sound server. ''; }; systemWide = lib.mkOption { type = lib.types.bool; default = false; description = '' If false, a PulseAudio server is launched automatically for each user that tries to use the sound system. The server runs with user privileges. If true, one system-wide PulseAudio server is launched on boot, running as the user "pulse", and only users in the "pulse-access" group will have access to the server. Please read the PulseAudio documentation for more details. Don't enable this option unless you know what you are doing. ''; }; support32Bit = lib.mkOption { type = lib.types.bool; default = false; description = '' Whether to include the 32-bit pulseaudio libraries in the system or not. This is only useful on 64-bit systems and currently limited to x86_64-linux. ''; }; configFile = lib.mkOption { type = lib.types.nullOr lib.types.path; description = '' The path to the default configuration options the PulseAudio server should use. By default, the "default.pa" configuration from the PulseAudio distribution is used. ''; }; extraConfig = lib.mkOption { type = lib.types.lines; default = ""; description = '' Literal string to append to `configFile` and the config file generated by the pulseaudio module. ''; }; extraClientConf = lib.mkOption { type = lib.types.lines; default = ""; description = '' Extra configuration appended to pulse/client.conf file. ''; }; package = lib.mkOption { type = lib.types.package; default = if config.services.jack.jackd.enable then pkgs.pulseaudioFull else pkgs.pulseaudio; defaultText = lib.literalExpression "pkgs.pulseaudio"; example = lib.literalExpression "pkgs.pulseaudioFull"; description = '' The PulseAudio derivation to use. This can be used to enable features (such as JACK support, Bluetooth) via the `pulseaudioFull` package. ''; }; extraModules = lib.mkOption { type = lib.types.listOf lib.types.package; default = []; example = lib.literalExpression "[ pkgs.pulseaudio-modules-bt ]"; description = '' Extra pulseaudio modules to use. This is intended for out-of-tree pulseaudio modules like extra bluetooth codecs. Extra modules take precedence over built-in pulseaudio modules. ''; }; daemon = { logLevel = lib.mkOption { type = lib.types.str; default = "notice"; description = '' The log level that the system-wide pulseaudio daemon should use, if activated. ''; }; config = lib.mkOption { type = lib.types.attrsOf lib.types.unspecified; default = {}; description = "Config of the pulse daemon. See `man pulse-daemon.conf`."; example = lib.literalExpression ''{ realtime-scheduling = "yes"; }''; }; }; zeroconf = { discovery.enable = lib.mkEnableOption "discovery of pulseaudio sinks in the local network"; publish.enable = lib.mkEnableOption "publishing the pulseaudio sink in the local network"; }; # TODO: enable by default? tcp = { enable = lib.mkEnableOption "tcp streaming support"; anonymousClients = { allowAll = lib.mkEnableOption "all anonymous clients to stream to the server"; allowedIpRanges = lib.mkOption { type = lib.types.listOf lib.types.str; default = []; example = lib.literalExpression ''[ "127.0.0.1" "192.168.1.0/24" ]''; description = '' A list of IP subnets that are allowed to stream to the server. ''; }; }; }; }; }; config = lib.mkIf cfg.enable (lib.mkMerge [ { environment.etc."pulse/client.conf".source = clientConf; environment.systemPackages = [ overriddenPackage ]; environment.etc = { "alsa/conf.d/99-pulseaudio.conf".text = alsaConf; "pulse/daemon.conf".source = pkgs.writeText "daemon.conf" (lib.generators.toKeyValue {} cfg.daemon.config); "openal/alsoft.conf".source = pkgs.writeText "alsoft.conf" "drivers=pulse"; "libao.conf".source = pkgs.writeText "libao.conf" "default_driver=pulse"; }; hardware.pulseaudio.configFile = lib.mkDefault "${lib.getBin overriddenPackage}/etc/pulse/default.pa"; # Disable flat volumes to enable relative ones hardware.pulseaudio.daemon.config.flat-volumes = lib.mkDefault "no"; # Upstream defaults to speex-float-1 which results in audible artifacts hardware.pulseaudio.daemon.config.resample-method = lib.mkDefault "speex-float-5"; # Allow PulseAudio to get realtime priority using rtkit. security.rtkit.enable = true; systemd.packages = [ overriddenPackage ]; # PulseAudio is packaged with udev rules to handle various audio device quirks services.udev.packages = [ overriddenPackage ]; } (lib.mkIf (cfg.extraModules != []) { hardware.pulseaudio.daemon.config.dl-search-path = let overriddenModules = builtins.map (drv: drv.override { pulseaudio = overriddenPackage; }) cfg.extraModules; modulePaths = builtins.map (drv: "${drv}/lib/pulseaudio/modules") # User-provided extra modules take precedence (overriddenModules ++ [ overriddenPackage ]); in lib.concatStringsSep ":" modulePaths; }) (lib.mkIf hasZeroconf { services.avahi.enable = true; }) (lib.mkIf cfg.zeroconf.publish.enable { services.avahi.publish.enable = true; services.avahi.publish.userServices = true; }) (lib.mkIf (!cfg.systemWide) { environment.etc = { "pulse/default.pa".source = myConfigFile; }; systemd.user = { services.pulseaudio = { restartIfChanged = true; serviceConfig = { RestartSec = "500ms"; PassEnvironment = "DISPLAY"; }; } // lib.optionalAttrs config.services.jack.jackd.enable { environment.JACK_PROMISCUOUS_SERVER = "jackaudio"; }; sockets.pulseaudio = { wantedBy = [ "sockets.target" ]; }; }; }) (lib.mkIf cfg.systemWide { users.users.pulse = { # For some reason, PulseAudio wants UID == GID. uid = assert uid == gid; uid; group = "pulse"; extraGroups = [ "audio" ]; description = "PulseAudio system service user"; home = stateDir; homeMode = "755"; createHome = true; isSystemUser = true; }; users.groups.pulse.gid = gid; users.groups.pulse-access = {}; systemd.services.pulseaudio = { description = "PulseAudio System-Wide Server"; wantedBy = [ "sound.target" ]; before = [ "sound.target" ]; environment.PULSE_RUNTIME_PATH = stateDir; serviceConfig = { Type = "notify"; ExecStart = "${binaryNoDaemon} --log-level=${cfg.daemon.logLevel} --system -n --file=${myConfigFile}"; Restart = "on-failure"; RestartSec = "500ms"; }; }; environment.variables.PULSE_COOKIE = "${stateDir}/.config/pulse/cookie"; }) ]); }