diff options
Diffstat (limited to 'nixpkgs/nixos/modules/services/audio')
29 files changed, 3743 insertions, 0 deletions
diff --git a/nixpkgs/nixos/modules/services/audio/alsa.nix b/nixpkgs/nixos/modules/services/audio/alsa.nix new file mode 100644 index 000000000000..155780199fd6 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/alsa.nix @@ -0,0 +1,133 @@ +# ALSA sound support. +{ config, lib, pkgs, ... }: + +with lib; + +let + + inherit (pkgs) alsa-utils; + + pulseaudioEnabled = config.hardware.pulseaudio.enable; + +in + +{ + imports = [ + (mkRenamedOptionModule [ "sound" "enableMediaKeys" ] [ "sound" "mediaKeys" "enable" ]) + ]; + + ###### interface + + options = { + + sound = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable ALSA sound. + ''; + }; + + enableOSSEmulation = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable ALSA OSS emulation (with certain cards sound mixing may not work!). + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + example = '' + defaults.pcm.!card 3 + ''; + description = lib.mdDoc '' + Set addition configuration for system-wide alsa. + ''; + }; + + mediaKeys = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable volume and capture control with keyboard media keys. + + You want to leave this disabled if you run a desktop environment + like KDE, Gnome, Xfce, etc, as those handle such things themselves. + You might want to enable this if you run a minimalistic desktop + environment or work from bare linux ttys/framebuffers. + + Enabling this will turn on {option}`services.actkbd`. + ''; + }; + + volumeStep = mkOption { + type = types.str; + default = "1"; + example = "1%"; + description = lib.mdDoc '' + The value by which to increment/decrement volume on media keys. + + See amixer(1) for allowed values. + ''; + }; + + }; + + }; + + }; + + + ###### implementation + + config = mkIf config.sound.enable { + + environment.systemPackages = [ alsa-utils ]; + + environment.etc = mkIf (!pulseaudioEnabled && config.sound.extraConfig != "") + { "asound.conf".text = config.sound.extraConfig; }; + + # ALSA provides a udev rule for restoring volume settings. + services.udev.packages = [ alsa-utils ]; + + boot.kernelModules = optional config.sound.enableOSSEmulation "snd_pcm_oss"; + + systemd.services.alsa-store = + { description = "Store Sound Card State"; + wantedBy = [ "multi-user.target" ]; + unitConfig.RequiresMountsFor = "/var/lib/alsa"; + unitConfig.ConditionVirtualization = "!systemd-nspawn"; + serviceConfig = { + Type = "oneshot"; + RemainAfterExit = true; + ExecStart = "${pkgs.coreutils}/bin/mkdir -p /var/lib/alsa"; + ExecStop = "${alsa-utils}/sbin/alsactl store --ignore"; + }; + }; + + services.actkbd = mkIf config.sound.mediaKeys.enable { + enable = true; + bindings = [ + # "Mute" media key + { keys = [ 113 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Master toggle"; } + + # "Lower Volume" media key + { keys = [ 114 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}- unmute"; } + + # "Raise Volume" media key + { keys = [ 115 ]; events = [ "key" "rep" ]; command = "${alsa-utils}/bin/amixer -q set Master ${config.sound.mediaKeys.volumeStep}+ unmute"; } + + # "Mic Mute" media key + { keys = [ 190 ]; events = [ "key" ]; command = "${alsa-utils}/bin/amixer -q set Capture toggle"; } + ]; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/botamusique.nix b/nixpkgs/nixos/modules/services/audio/botamusique.nix new file mode 100644 index 000000000000..42227cb14722 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/botamusique.nix @@ -0,0 +1,109 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.botamusique; + + format = pkgs.formats.ini {}; + configFile = format.generate "botamusique.ini" cfg.settings; +in +{ + meta.maintainers = with lib.maintainers; [ hexa ]; + + options.services.botamusique = { + enable = mkEnableOption (lib.mdDoc "botamusique, a bot to play audio streams on mumble"); + + package = mkPackageOption pkgs "botamusique" { }; + + settings = mkOption { + type = with types; submodule { + freeformType = format.type; + options = { + server.host = mkOption { + type = types.str; + default = "localhost"; + example = "mumble.example.com"; + description = lib.mdDoc "Hostname of the mumble server to connect to."; + }; + + server.port = mkOption { + type = types.port; + default = 64738; + description = lib.mdDoc "Port of the mumble server to connect to."; + }; + + bot.username = mkOption { + type = types.str; + default = "botamusique"; + description = lib.mdDoc "Name the bot should appear with."; + }; + + bot.comment = mkOption { + type = types.str; + default = "Hi, I'm here to play radio, local music or youtube/soundcloud music. Have fun!"; + description = lib.mdDoc "Comment displayed for the bot."; + }; + }; + }; + default = {}; + description = lib.mdDoc '' + Your {file}`configuration.ini` as a Nix attribute set. Look up + possible options in the [configuration.example.ini](https://github.com/azlux/botamusique/blob/master/configuration.example.ini). + ''; + }; + }; + + config = mkIf cfg.enable { + systemd.services.botamusique = { + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + + unitConfig.Documentation = "https://github.com/azlux/botamusique/wiki"; + + environment.HOME = "/var/lib/botamusique"; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/botamusique --config ${configFile}"; + Restart = "always"; # the bot exits when the server connection is lost + + # Hardening + CapabilityBoundingSet = [ "" ]; + DynamicUser = true; + IPAddressDeny = [ + "link-local" + "multicast" + ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + ProcSubset = "pid"; + PrivateDevices = true; + PrivateUsers = true; + PrivateTmp = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + ProtectSystem = "strict"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + StateDirectory = "botamusique"; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service @resources" + "~@privileged" + ]; + UMask = "0077"; + WorkingDirectory = "/var/lib/botamusique"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/castopod.md b/nixpkgs/nixos/modules/services/audio/castopod.md new file mode 100644 index 000000000000..ee8590737a7c --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/castopod.md @@ -0,0 +1,22 @@ +# Castopod {#module-services-castopod} + +Castopod is an open-source hosting platform made for podcasters who want to engage and interact with their audience. + +## Quickstart {#module-services-castopod-quickstart} + +Use the following configuration to start a public instance of Castopod on `castopod.example.com` domain: + +```nix +networking.firewall.allowedTCPPorts = [ 80 443 ]; +services.castopod = { + enable = true; + database.createLocally = true; + nginx.virtualHost = { + serverName = "castopod.example.com"; + enableACME = true; + forceSSL = true; + }; +}; +``` + +Go to `https://castopod.example.com/cp-install` to create superadmin account after applying the above configuration. diff --git a/nixpkgs/nixos/modules/services/audio/castopod.nix b/nixpkgs/nixos/modules/services/audio/castopod.nix new file mode 100644 index 000000000000..b782b5489147 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/castopod.nix @@ -0,0 +1,287 @@ +{ config, lib, pkgs, ... }: +let + cfg = config.services.castopod; + fpm = config.services.phpfpm.pools.castopod; + + user = "castopod"; + stateDirectory = "/var/lib/castopod"; + + # https://docs.castopod.org/getting-started/install.html#requirements + phpPackage = pkgs.php.withExtensions ({ enabled, all }: with all; [ + intl + curl + mbstring + gd + exif + mysqlnd + ] ++ enabled); +in +{ + meta.doc = ./castopod.md; + meta.maintainers = with lib.maintainers; [ alexoundos misuzu ]; + + options.services = { + castopod = { + enable = lib.mkEnableOption (lib.mdDoc "Castopod"); + package = lib.mkOption { + type = lib.types.package; + default = pkgs.castopod; + defaultText = lib.literalMD "pkgs.castopod"; + description = lib.mdDoc "Which Castopod package to use."; + }; + database = { + createLocally = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc '' + Create the database and database user locally. + ''; + }; + hostname = lib.mkOption { + type = lib.types.str; + default = "localhost"; + description = lib.mdDoc "Database hostname."; + }; + name = lib.mkOption { + type = lib.types.str; + default = "castopod"; + description = lib.mdDoc "Database name."; + }; + user = lib.mkOption { + type = lib.types.str; + default = user; + description = lib.mdDoc "Database user."; + }; + passwordFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/castopod-dbpassword"; + description = lib.mdDoc '' + A file containing the password corresponding to + [](#opt-services.castopod.database.user). + ''; + }; + }; + settings = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ str int bool ]); + default = { }; + example = { + "email.protocol" = "smtp"; + "email.SMTPHost" = "localhost"; + "email.SMTPUser" = "myuser"; + "email.fromEmail" = "castopod@example.com"; + }; + description = lib.mdDoc '' + Environment variables used for Castopod. + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) + for available environment variables. + ''; + }; + environmentFile = lib.mkOption { + type = lib.types.nullOr lib.types.path; + default = null; + example = "/run/keys/castopod-env"; + description = lib.mdDoc '' + Environment file to inject e.g. secrets into the configuration. + See [](https://code.castopod.org/adaures/castopod/-/blob/main/.env.example) + for available environment variables. + ''; + }; + configureNginx = lib.mkOption { + type = lib.types.bool; + default = true; + description = lib.mdDoc "Configure nginx as a reverse proxy for CastoPod."; + }; + localDomain = lib.mkOption { + type = lib.types.str; + example = "castopod.example.org"; + description = lib.mdDoc "The domain serving your CastoPod instance."; + }; + poolSettings = lib.mkOption { + type = with lib.types; attrsOf (oneOf [ str int bool ]); + default = { + "pm" = "dynamic"; + "pm.max_children" = "32"; + "pm.start_servers" = "2"; + "pm.min_spare_servers" = "2"; + "pm.max_spare_servers" = "4"; + "pm.max_requests" = "500"; + }; + description = lib.mdDoc '' + Options for Castopod's PHP pool. See the documentation on `php-fpm.conf` for details on configuration directives. + ''; + }; + }; + }; + + config = lib.mkIf cfg.enable { + services.castopod.settings = + let + sslEnabled = with config.services.nginx.virtualHosts.${cfg.localDomain}; addSSL || forceSSL || onlySSL || enableACME || useACMEHost != null; + baseURL = "http${lib.optionalString sslEnabled "s"}://${cfg.localDomain}"; + in + lib.mapAttrs (name: lib.mkDefault) { + "app.forceGlobalSecureRequests" = sslEnabled; + "app.baseURL" = baseURL; + + "media.baseURL" = "/"; + "media.root" = "media"; + "media.storage" = stateDirectory; + + "admin.gateway" = "admin"; + "auth.gateway" = "auth"; + + "database.default.hostname" = cfg.database.hostname; + "database.default.database" = cfg.database.name; + "database.default.username" = cfg.database.user; + "database.default.DBPrefix" = "cp_"; + + "cache.handler" = "file"; + }; + + services.phpfpm.pools.castopod = { + inherit user; + group = config.services.nginx.group; + phpPackage = phpPackage; + phpOptions = '' + # https://code.castopod.org/adaures/castopod/-/blob/main/docker/production/app/uploads.ini + file_uploads = On + memory_limit = 512M + upload_max_filesize = 500M + post_max_size = 512M + max_execution_time = 300 + max_input_time = 300 + ''; + settings = { + "listen.owner" = config.services.nginx.user; + "listen.group" = config.services.nginx.group; + } // cfg.poolSettings; + }; + + systemd.services.castopod-setup = { + after = lib.optional config.services.mysql.enable "mysql.service"; + requires = lib.optional config.services.mysql.enable "mysql.service"; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.openssl phpPackage ]; + script = + let + envFile = "${stateDirectory}/.env"; + media = "${cfg.settings."media.storage"}/${cfg.settings."media.root"}"; + in + '' + mkdir -p ${stateDirectory}/writable/{cache,logs,session,temp,uploads} + + if [ ! -d ${lib.escapeShellArg media} ]; then + cp --no-preserve=mode,ownership -r ${cfg.package}/share/castopod/public/media ${lib.escapeShellArg media} + fi + + if [ ! -f ${stateDirectory}/salt ]; then + openssl rand -base64 33 > ${stateDirectory}/salt + fi + + cat <<'EOF' > ${envFile} + ${lib.generators.toKeyValue { } cfg.settings} + EOF + + echo "analytics.salt=$(cat ${stateDirectory}/salt)" >> ${envFile} + + ${if (cfg.database.passwordFile != null) then '' + echo "database.default.password=$(cat ${lib.escapeShellArg cfg.database.passwordFile})" >> ${envFile} + '' else '' + echo "database.default.password=" >> ${envFile} + ''} + + ${lib.optionalString (cfg.environmentFile != null) '' + cat ${lib.escapeShellArg cfg.environmentFile}) >> ${envFile} + ''} + + php spark castopod:database-update + ''; + serviceConfig = { + StateDirectory = "castopod"; + WorkingDirectory = "${cfg.package}/share/castopod"; + Type = "oneshot"; + RemainAfterExit = true; + User = user; + Group = config.services.nginx.group; + }; + }; + + systemd.services.castopod-scheduled = { + after = [ "castopod-setup.service" ]; + wantedBy = [ "multi-user.target" ]; + path = [ phpPackage ]; + script = '' + php public/index.php scheduled-activities + php public/index.php scheduled-websub-publish + php public/index.php scheduled-video-clips + ''; + serviceConfig = { + StateDirectory = "castopod"; + WorkingDirectory = "${cfg.package}/share/castopod"; + Type = "oneshot"; + User = user; + Group = config.services.nginx.group; + }; + }; + + systemd.timers.castopod-scheduled = { + wantedBy = [ "timers.target" ]; + timerConfig = { + OnCalendar = "*-*-* *:*:00"; + Unit = "castopod-scheduled.service"; + }; + }; + + services.mysql = lib.mkIf cfg.database.createLocally { + enable = true; + package = lib.mkDefault pkgs.mariadb; + ensureDatabases = [ cfg.database.name ]; + ensureUsers = [{ + name = cfg.database.user; + ensurePermissions = { "${cfg.database.name}.*" = "ALL PRIVILEGES"; }; + }]; + }; + + services.nginx = lib.mkIf cfg.configureNginx { + enable = true; + virtualHosts."${cfg.localDomain}" = { + root = lib.mkForce "${cfg.package}/share/castopod/public"; + + extraConfig = '' + try_files $uri $uri/ /index.php?$args; + index index.php index.html; + ''; + + locations."^~ /${cfg.settings."media.root"}/" = { + root = cfg.settings."media.storage"; + extraConfig = '' + add_header Access-Control-Allow-Origin "*"; + expires max; + access_log off; + ''; + }; + + locations."~ \.php$" = { + fastcgiParams = { + SERVER_NAME = "$host"; + }; + extraConfig = '' + fastcgi_intercept_errors on; + fastcgi_index index.php; + fastcgi_pass unix:${fpm.socket}; + try_files $uri =404; + fastcgi_read_timeout 3600; + fastcgi_send_timeout 3600; + ''; + }; + }; + }; + + users.users.${user} = lib.mapAttrs (name: lib.mkDefault) { + description = "Castopod user"; + isSystemUser = true; + group = config.services.nginx.group; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/gmediarender.nix b/nixpkgs/nixos/modules/services/audio/gmediarender.nix new file mode 100644 index 000000000000..a4cb89098db7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/gmediarender.nix @@ -0,0 +1,117 @@ +{ pkgs, lib, config, utils, ... }: + +with lib; + +let + cfg = config.services.gmediarender; +in +{ + options.services.gmediarender = { + enable = mkEnableOption (mdDoc "the gmediarender DLNA renderer"); + + audioDevice = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + The audio device to use. + ''; + }; + + audioSink = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + The audio sink to use. + ''; + }; + + friendlyName = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + A "friendly name" for identifying the endpoint. + ''; + }; + + initialVolume = mkOption { + type = types.nullOr types.int; + default = 0; + description = mdDoc '' + A default volume attenuation (in dB) for the endpoint. + ''; + }; + + package = mkPackageOption pkgs "gmediarender" { + default = "gmrender-resurrect"; + }; + + port = mkOption { + type = types.nullOr types.port; + default = null; + description = mdDoc "Port that will be used to accept client connections."; + }; + + uuid = mkOption { + type = types.nullOr types.str; + default = null; + description = mdDoc '' + A UUID for uniquely identifying the endpoint. If you have + multiple renderers on your network, you MUST set this. + ''; + }; + }; + + config = mkIf cfg.enable { + systemd = { + services.gmediarender = { + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + wantedBy = [ "multi-user.target" ]; + description = "gmediarender server daemon"; + environment = { + XDG_CACHE_HOME = "%t/gmediarender"; + }; + serviceConfig = { + DynamicUser = true; + User = "gmediarender"; + Group = "gmediarender"; + SupplementaryGroups = [ "audio" ]; + ExecStart = + "${cfg.package}/bin/gmediarender " + + optionalString (cfg.audioDevice != null) ("--gstout-audiodevice=${utils.escapeSystemdExecArg cfg.audioDevice} ") + + optionalString (cfg.audioSink != null) ("--gstout-audiosink=${utils.escapeSystemdExecArg cfg.audioSink} ") + + optionalString (cfg.friendlyName != null) ("--friendly-name=${utils.escapeSystemdExecArg cfg.friendlyName} ") + + optionalString (cfg.initialVolume != 0) ("--initial-volume=${toString cfg.initialVolume} ") + + optionalString (cfg.port != null) ("--port=${toString cfg.port} ") + + optionalString (cfg.uuid != null) ("--uuid=${utils.escapeSystemdExecArg cfg.uuid} "); + Restart = "always"; + RuntimeDirectory = "gmediarender"; + + # Security options: + CapabilityBoundingSet = ""; + LockPersonality = true; + MemoryDenyWriteExecute = true; + NoNewPrivileges = true; + # PrivateDevices = true; + PrivateTmp = true; + PrivateUsers = true; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + UMask = 066; + }; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/gonic.nix b/nixpkgs/nixos/modules/services/audio/gonic.nix new file mode 100644 index 000000000000..66daeb60b503 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/gonic.nix @@ -0,0 +1,90 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.gonic; + settingsFormat = pkgs.formats.keyValue { + mkKeyValue = lib.generators.mkKeyValueDefault { } " "; + listsAsDuplicateKeys = true; + }; +in +{ + options = { + services.gonic = { + + enable = mkEnableOption (lib.mdDoc "Gonic music server"); + + settings = mkOption rec { + type = settingsFormat.type; + apply = recursiveUpdate default; + default = { + listen-addr = "127.0.0.1:4747"; + cache-path = "/var/cache/gonic"; + tls-cert = null; + tls-key = null; + }; + example = { + music-path = [ "/mnt/music" ]; + podcast-path = "/mnt/podcasts"; + }; + description = lib.mdDoc '' + Configuration for Gonic, see <https://github.com/sentriz/gonic#configuration-options> for supported values. + ''; + }; + + }; + }; + + config = mkIf cfg.enable { + systemd.services.gonic = { + description = "Gonic Media Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = + let + # these values are null by default but should not appear in the final config + filteredSettings = filterAttrs (n: v: !((n == "tls-cert" || n == "tls-key") && v == null)) cfg.settings; + in + "${pkgs.gonic}/bin/gonic -config-path ${settingsFormat.generate "gonic" filteredSettings}"; + DynamicUser = true; + StateDirectory = "gonic"; + CacheDirectory = "gonic"; + WorkingDirectory = "/var/lib/gonic"; + RuntimeDirectory = "gonic"; + RootDirectory = "/run/gonic"; + ReadWritePaths = ""; + BindReadOnlyPaths = [ + # gonic can access scrobbling services + "-/etc/resolv.conf" + "-/etc/ssl/certs/ca-certificates.crt" + builtins.storeDir + cfg.settings.podcast-path + ] ++ cfg.settings.music-path + ++ lib.optional (cfg.settings.tls-cert != null) cfg.settings.tls-cert + ++ lib.optional (cfg.settings.tls-key != null) cfg.settings.tls-key; + CapabilityBoundingSet = ""; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + RestrictRealtime = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + UMask = "0066"; + ProtectHostname = true; + }; + }; + }; + + meta.maintainers = [ maintainers.autrimpo ]; +} diff --git a/nixpkgs/nixos/modules/services/audio/goxlr-utility.nix b/nixpkgs/nixos/modules/services/audio/goxlr-utility.nix new file mode 100644 index 000000000000..c047dbb221b1 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/goxlr-utility.nix @@ -0,0 +1,48 @@ +{ config, lib, pkgs, ... }: + +let + cfg = config.services.goxlr-utility; +in + +with lib; +{ + + options = { + services.goxlr-utility = { + enable = mkOption { + default = false; + type = types.bool; + description = lib.mdDoc '' + Whether to enable goxlr-utility for controlling your TC-Helicon GoXLR or GoXLR Mini + ''; + }; + package = mkPackageOption pkgs "goxlr-utility" { }; + autoStart.xdg = mkOption { + default = true; + type = with types; bool; + description = lib.mdDoc '' + Start the daemon automatically using XDG autostart. + Sets `xdg.autostart.enable = true` if not already enabled. + ''; + }; + }; + }; + + config = mkIf config.services.goxlr-utility.enable + { + services.udev.packages = [ cfg.package ]; + + xdg.autostart.enable = mkIf cfg.autoStart.xdg true; + environment.systemPackages = mkIf cfg.autoStart.xdg + [ + cfg.package + (pkgs.makeAutostartItem + { + name = "goxlr-utility"; + package = cfg.package; + }) + ]; + }; + + meta.maintainers = with maintainers; [ errnoh ]; +} diff --git a/nixpkgs/nixos/modules/services/audio/hqplayerd.nix b/nixpkgs/nixos/modules/services/audio/hqplayerd.nix new file mode 100644 index 000000000000..d54400b18e30 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/hqplayerd.nix @@ -0,0 +1,139 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.hqplayerd; + pkg = pkgs.hqplayerd; + # XXX: This is hard-coded in the distributed binary, don't try to change it. + stateDir = "/var/lib/hqplayer"; + configDir = "/etc/hqplayer"; +in +{ + options = { + services.hqplayerd = { + enable = mkEnableOption (lib.mdDoc "HQPlayer Embedded"); + + auth = { + username = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Username used for HQPlayer's WebUI. + + Without this you will need to manually create the credentials after + first start by going to http://your.ip/8088/auth + ''; + }; + + password = mkOption { + type = types.nullOr types.str; + default = null; + description = lib.mdDoc '' + Password used for HQPlayer's WebUI. + + Without this you will need to manually create the credentials after + first start by going to http://your.ip/8088/auth + ''; + }; + }; + + licenseFile = mkOption { + type = types.nullOr types.path; + default = null; + description = lib.mdDoc '' + Path to the HQPlayer license key file. + + Without this, the service will run in trial mode and restart every 30 + minutes. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Opens ports needed for the WebUI and controller API. + ''; + }; + + config = mkOption { + type = types.nullOr types.lines; + default = null; + description = lib.mdDoc '' + HQplayer daemon configuration, written to /etc/hqplayer/hqplayerd.xml. + + Refer to share/doc/hqplayerd/readme.txt in the hqplayerd derivation for possible values. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = (cfg.auth.username != null -> cfg.auth.password != null) + && (cfg.auth.password != null -> cfg.auth.username != null); + message = "You must set either both services.hqplayer.auth.username and password, or neither."; + } + ]; + + environment = { + etc = { + "hqplayer/hqplayerd.xml" = mkIf (cfg.config != null) { source = pkgs.writeText "hqplayerd.xml" cfg.config; }; + "hqplayer/hqplayerd4-key.xml" = mkIf (cfg.licenseFile != null) { source = cfg.licenseFile; }; + }; + systemPackages = [ pkg ]; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPorts = [ 8088 4321 ]; + }; + + systemd = { + tmpfiles.rules = [ + "d ${configDir} 0755 hqplayer hqplayer - -" + "d ${stateDir} 0755 hqplayer hqplayer - -" + "d ${stateDir}/home 0755 hqplayer hqplayer - -" + ]; + + packages = [ pkg ]; + + services.hqplayerd = { + wantedBy = [ "multi-user.target" ]; + after = [ "systemd-tmpfiles-setup.service" ]; + + environment.HOME = "${stateDir}/home"; + + unitConfig.ConditionPathExists = [ configDir stateDir ]; + + restartTriggers = optionals (cfg.config != null) [ config.environment.etc."hqplayer/hqplayerd.xml".source ]; + + preStart = '' + cp -r "${pkg}/var/lib/hqplayer/web" "${stateDir}" + chmod -R u+wX "${stateDir}/web" + + if [ ! -f "${configDir}/hqplayerd.xml" ]; then + echo "creating initial config file" + install -m 0644 "${pkg}/etc/hqplayer/hqplayerd.xml" "${configDir}/hqplayerd.xml" + fi + '' + optionalString (cfg.auth.username != null && cfg.auth.password != null) '' + ${pkg}/bin/hqplayerd -s ${cfg.auth.username} ${cfg.auth.password} + ''; + }; + }; + + users.groups = { + hqplayer.gid = config.ids.gids.hqplayer; + }; + + users.users = { + hqplayer = { + description = "hqplayer daemon user"; + extraGroups = [ "audio" "video" ]; + group = "hqplayer"; + uid = config.ids.uids.hqplayer; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/icecast.nix b/nixpkgs/nixos/modules/services/audio/icecast.nix new file mode 100644 index 000000000000..63049bd93ab9 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/icecast.nix @@ -0,0 +1,131 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.icecast; + configFile = pkgs.writeText "icecast.xml" '' + <icecast> + <hostname>${cfg.hostname}</hostname> + + <authentication> + <admin-user>${cfg.admin.user}</admin-user> + <admin-password>${cfg.admin.password}</admin-password> + </authentication> + + <paths> + <logdir>${cfg.logDir}</logdir> + <adminroot>${pkgs.icecast}/share/icecast/admin</adminroot> + <webroot>${pkgs.icecast}/share/icecast/web</webroot> + <alias source="/" dest="/status.xsl"/> + </paths> + + <listen-socket> + <port>${toString cfg.listen.port}</port> + <bind-address>${cfg.listen.address}</bind-address> + </listen-socket> + + <security> + <chroot>0</chroot> + <changeowner> + <user>${cfg.user}</user> + <group>${cfg.group}</group> + </changeowner> + </security> + + ${cfg.extraConf} + </icecast> + ''; +in { + + ###### interface + + options = { + + services.icecast = { + + enable = mkEnableOption (lib.mdDoc "Icecast server"); + + hostname = mkOption { + type = types.nullOr types.str; + description = lib.mdDoc "DNS name or IP address that will be used for the stream directory lookups or possibly the playlist generation if a Host header is not provided."; + default = config.networking.domain; + defaultText = literalExpression "config.networking.domain"; + }; + + admin = { + user = mkOption { + type = types.str; + description = lib.mdDoc "Username used for all administration functions."; + default = "admin"; + }; + + password = mkOption { + type = types.str; + description = lib.mdDoc "Password used for all administration functions."; + }; + }; + + logDir = mkOption { + type = types.path; + description = lib.mdDoc "Base directory used for logging."; + default = "/var/log/icecast"; + }; + + listen = { + port = mkOption { + type = types.port; + description = lib.mdDoc "TCP port that will be used to accept client connections."; + default = 8000; + }; + + address = mkOption { + type = types.str; + description = lib.mdDoc "Address Icecast will listen on."; + default = "::"; + }; + }; + + user = mkOption { + type = types.str; + description = lib.mdDoc "User privileges for the server."; + default = "nobody"; + }; + + group = mkOption { + type = types.str; + description = lib.mdDoc "Group privileges for the server."; + default = "nogroup"; + }; + + extraConf = mkOption { + type = types.lines; + description = lib.mdDoc "icecast.xml content."; + default = ""; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + systemd.services.icecast = { + after = [ "network.target" ]; + description = "Icecast Network Audio Streaming Server"; + wantedBy = [ "multi-user.target" ]; + + preStart = "mkdir -p ${cfg.logDir} && chown ${cfg.user}:${cfg.group} ${cfg.logDir}"; + serviceConfig = { + Type = "simple"; + ExecStart = "${pkgs.icecast}/bin/icecast -c ${configFile}"; + ExecReload = "${pkgs.coreutils}/bin/kill -HUP $MAINPID"; + }; + }; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/jack.nix b/nixpkgs/nixos/modules/services/audio/jack.nix new file mode 100644 index 000000000000..3869bd974cce --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/jack.nix @@ -0,0 +1,289 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.jack; + + pcmPlugin = cfg.jackd.enable && cfg.alsa.enable; + loopback = cfg.jackd.enable && cfg.loopback.enable; + + enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null; + + umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12"; + bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12"; +in { + options = { + services.jack = { + jackd = { + enable = mkEnableOption (lib.mdDoc '' + JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group + ''); + + package = mkPackageOption pkgs "jack2" { + example = "jack1"; + } // { + # until jack1 promiscuous mode is fixed + internal = true; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ + "-dalsa" + ]; + example = literalExpression '' + [ "-dalsa" "--device" "hw:1" ]; + ''; + description = lib.mdDoc '' + Specifies startup command line arguments to pass to JACK server. + ''; + }; + + session = mkOption { + type = types.lines; + description = lib.mdDoc '' + Commands to run after JACK is started. + ''; + }; + + }; + + alsa = { + enable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin. + ''; + }; + + support32Bit = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to support sound for 32-bit ALSA applications on 64-bit system. + ''; + }; + }; + + loopback = { + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Create ALSA loopback device, instead of using PCM plugin. Has broader + application support (things like Steam will work), but may need fine-tuning + for concrete hardware. + ''; + }; + + index = mkOption { + type = types.int; + default = 10; + description = lib.mdDoc '' + Index of an ALSA loopback device. + ''; + }; + + config = mkOption { + type = types.lines; + description = lib.mdDoc '' + ALSA config for loopback device. + ''; + }; + + dmixConfig = mkOption { + type = types.lines; + default = ""; + example = '' + period_size 2048 + periods 2 + ''; + description = lib.mdDoc '' + For music production software that still doesn't support JACK natively you + would like to put buffer/period adjustments here + to decrease dmix device latency. + ''; + }; + + session = mkOption { + type = types.lines; + description = lib.mdDoc '' + Additional commands to run to setup loopback device. + ''; + }; + }; + + }; + + }; + + config = mkMerge [ + + (mkIf pcmPlugin { + sound.extraConfig = '' + pcm_type.jack { + libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ; + ${lib.optionalString enable32BitAlsaPlugins + "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"} + } + pcm.!default { + @func getenv + vars [ PCM ] + default "plug:jack" + } + ''; + }) + + (mkIf loopback { + boot.kernelModules = [ "snd-aloop" ]; + boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ]; + sound.extraConfig = cfg.loopback.config; + }) + + (mkIf cfg.jackd.enable { + services.jack.jackd.session = '' + ${lib.optionalString bridgeNeeded "${pkgs.a2jmidid}/bin/a2jmidid -e &"} + ''; + # https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge#id06 + services.jack.loopback.config = '' + pcm.loophw00 { + type hw + card ${toString cfg.loopback.index} + device 0 + subdevice 0 + } + pcm.amix { + type dmix + ipc_key 219345 + slave { + pcm loophw00 + ${cfg.loopback.dmixConfig} + } + } + pcm.asoftvol { + type softvol + slave.pcm "amix" + control { name Master } + } + pcm.cloop { + type hw + card ${toString cfg.loopback.index} + device 1 + subdevice 0 + format S32_LE + } + pcm.loophw01 { + type hw + card ${toString cfg.loopback.index} + device 0 + subdevice 1 + } + pcm.ploop { + type hw + card ${toString cfg.loopback.index} + device 1 + subdevice 1 + format S32_LE + } + pcm.aduplex { + type asym + playback.pcm "asoftvol" + capture.pcm "loophw01" + } + pcm.!default { + type plug + slave.pcm aduplex + } + ''; + services.jack.loopback.session = '' + alsa_in -j cloop -dcloop & + alsa_out -j ploop -dploop & + while [ "$(jack_lsp cloop)" == "" ] || [ "$(jack_lsp ploop)" == "" ]; do sleep 1; done + jack_connect cloop:capture_1 system:playback_1 + jack_connect cloop:capture_2 system:playback_2 + jack_connect system:capture_1 ploop:playback_1 + jack_connect system:capture_2 ploop:playback_2 + ''; + + assertions = [ + { + assertion = !(cfg.alsa.enable && cfg.loopback.enable); + message = "For JACK both alsa and loopback options shouldn't be used at the same time."; + } + ]; + + users.users.jackaudio = { + group = "jackaudio"; + extraGroups = [ "audio" ]; + description = "JACK Audio system service user"; + isSystemUser = true; + }; + # https://jackaudio.org/faq/linux_rt_config.html + security.pam.loginLimits = [ + { domain = "@jackaudio"; type = "-"; item = "rtprio"; value = "99"; } + { domain = "@jackaudio"; type = "-"; item = "memlock"; value = "unlimited"; } + ]; + users.groups.jackaudio = {}; + + environment = { + systemPackages = [ cfg.jackd.package ]; + etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf"; + variables.JACK_PROMISCUOUS_SERVER = "jackaudio"; + }; + + services.udev.extraRules = '' + ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service" + ''; + + systemd.services.jack = { + description = "JACK Audio Connection Kit"; + serviceConfig = { + User = "jackaudio"; + SupplementaryGroups = lib.optional + (config.hardware.pulseaudio.enable + && !config.hardware.pulseaudio.systemWide) "users"; + ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}"; + LimitRTPRIO = 99; + LimitMEMLOCK = "infinity"; + } // optionalAttrs umaskNeeded { + UMask = "007"; + }; + path = [ cfg.jackd.package ]; + environment = { + JACK_PROMISCUOUS_SERVER = "jackaudio"; + JACK_NO_AUDIO_RESERVATION = "1"; + }; + restartIfChanged = false; + }; + systemd.services.jack-session = { + description = "JACK session"; + script = '' + jack_wait -w + ${cfg.jackd.session} + ${lib.optionalString cfg.loopback.enable cfg.loopback.session} + ''; + serviceConfig = { + RemainAfterExit = true; + User = "jackaudio"; + StateDirectory = "jack"; + LimitRTPRIO = 99; + LimitMEMLOCK = "infinity"; + }; + path = [ cfg.jackd.package ]; + environment = { + JACK_PROMISCUOUS_SERVER = "jackaudio"; + HOME = "/var/lib/jack"; + }; + wantedBy = [ "jack.service" ]; + partOf = [ "jack.service" ]; + after = [ "jack.service" ]; + restartIfChanged = false; + }; + }) + + ]; + + meta.maintainers = [ ]; +} diff --git a/nixpkgs/nixos/modules/services/audio/jmusicbot.nix b/nixpkgs/nixos/modules/services/audio/jmusicbot.nix new file mode 100644 index 000000000000..e7803677d0fd --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/jmusicbot.nix @@ -0,0 +1,44 @@ +{ config, lib, pkgs, ... }: + +with lib; +let + cfg = config.services.jmusicbot; +in +{ + options = { + services.jmusicbot = { + enable = mkEnableOption (lib.mdDoc "jmusicbot, a Discord music bot that's easy to set up and run yourself"); + + package = mkPackageOption pkgs "jmusicbot" { }; + + stateDir = mkOption { + type = types.path; + description = lib.mdDoc '' + The directory where config.txt and serversettings.json is saved. + If left as the default value this directory will automatically be created before JMusicBot starts, otherwise the sysadmin is responsible for ensuring the directory exists with appropriate ownership and permissions. + Untouched by the value of this option config.txt needs to be placed manually into this directory. + ''; + default = "/var/lib/jmusicbot/"; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.jmusicbot = { + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + description = "Discord music bot that's easy to set up and run yourself!"; + serviceConfig = mkMerge [{ + ExecStart = "${cfg.package}/bin/JMusicBot"; + WorkingDirectory = cfg.stateDir; + Restart = "always"; + RestartSec = 20; + DynamicUser = true; + } + (mkIf (cfg.stateDir == "/var/lib/jmusicbot") { StateDirectory = "jmusicbot"; })]; + }; + }; + + meta.maintainers = with maintainers; [ ]; +} diff --git a/nixpkgs/nixos/modules/services/audio/liquidsoap.nix b/nixpkgs/nixos/modules/services/audio/liquidsoap.nix new file mode 100644 index 000000000000..9e61a7979619 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/liquidsoap.nix @@ -0,0 +1,72 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + streams = builtins.attrNames config.services.liquidsoap.streams; + + streamService = + name: + let stream = builtins.getAttr name config.services.liquidsoap.streams; in + { inherit name; + value = { + after = [ "network-online.target" "sound.target" ]; + description = "${name} liquidsoap stream"; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.wget ]; + serviceConfig = { + ExecStart = "${pkgs.liquidsoap}/bin/liquidsoap ${stream}"; + User = "liquidsoap"; + LogsDirectory = "liquidsoap"; + Restart = "always"; + }; + }; + }; +in +{ + + ##### interface + + options = { + + services.liquidsoap.streams = mkOption { + + description = + lib.mdDoc '' + Set of Liquidsoap streams to start, + one systemd service per stream. + ''; + + default = {}; + + example = literalExpression '' + { + myStream1 = "/etc/liquidsoap/myStream1.liq"; + myStream2 = ./myStream2.liq; + myStream3 = "out(playlist(\"/srv/music/\"))"; + } + ''; + + type = types.attrsOf (types.either types.path types.str); + }; + + }; + ##### implementation + + config = mkIf (builtins.length streams != 0) { + + users.users.liquidsoap = { + uid = config.ids.uids.liquidsoap; + group = "liquidsoap"; + extraGroups = [ "audio" ]; + description = "Liquidsoap streaming user"; + home = "/var/lib/liquidsoap"; + createHome = true; + }; + + users.groups.liquidsoap.gid = config.ids.gids.liquidsoap; + + systemd.services = builtins.listToAttrs ( map streamService streams ); + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/mopidy.nix b/nixpkgs/nixos/modules/services/audio/mopidy.nix new file mode 100644 index 000000000000..8eebf0f9d1e1 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/mopidy.nix @@ -0,0 +1,109 @@ +{ config, lib, pkgs, ... }: + +with pkgs; +with lib; + +let + uid = config.ids.uids.mopidy; + gid = config.ids.gids.mopidy; + cfg = config.services.mopidy; + + mopidyConf = writeText "mopidy.conf" cfg.configuration; + + mopidyEnv = buildEnv { + name = "mopidy-with-extensions-${mopidy.version}"; + paths = closePropagation cfg.extensionPackages; + pathsToLink = [ "/${mopidyPackages.python.sitePackages}" ]; + nativeBuildInputs = [ makeWrapper ]; + postBuild = '' + makeWrapper ${mopidy}/bin/mopidy $out/bin/mopidy \ + --prefix PYTHONPATH : $out/${mopidyPackages.python.sitePackages} + ''; + }; +in { + + options = { + + services.mopidy = { + + enable = mkEnableOption (lib.mdDoc "Mopidy, a music player daemon"); + + dataDir = mkOption { + default = "/var/lib/mopidy"; + type = types.str; + description = lib.mdDoc '' + The directory where Mopidy stores its state. + ''; + }; + + extensionPackages = mkOption { + default = []; + type = types.listOf types.package; + example = literalExpression "[ pkgs.mopidy-spotify ]"; + description = lib.mdDoc '' + Mopidy extensions that should be loaded by the service. + ''; + }; + + configuration = mkOption { + default = ""; + type = types.lines; + description = lib.mdDoc '' + The configuration that Mopidy should use. + ''; + }; + + extraConfigFiles = mkOption { + default = []; + type = types.listOf types.str; + description = lib.mdDoc '' + Extra config file read by Mopidy when the service starts. + Later files in the list overrides earlier configuration. + ''; + }; + + }; + + }; + + ###### implementation + + config = mkIf cfg.enable { + + systemd.tmpfiles.settings."10-mopidy".${cfg.dataDir}.d = { + user = "mopidy"; + group = "mopidy"; + }; + + systemd.services.mopidy = { + wantedBy = [ "multi-user.target" ]; + after = [ "network-online.target" "sound.target" ]; + description = "mopidy music player daemon"; + serviceConfig = { + ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)}"; + User = "mopidy"; + }; + }; + + systemd.services.mopidy-scan = { + description = "mopidy local files scanner"; + serviceConfig = { + ExecStart = "${mopidyEnv}/bin/mopidy --config ${concatStringsSep ":" ([mopidyConf] ++ cfg.extraConfigFiles)} local scan"; + User = "mopidy"; + Type = "oneshot"; + }; + }; + + users.users.mopidy = { + inherit uid; + group = "mopidy"; + extraGroups = [ "audio" ]; + description = "Mopidy daemon user"; + home = cfg.dataDir; + }; + + users.groups.mopidy.gid = gid; + + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/mpd.nix b/nixpkgs/nixos/modules/services/audio/mpd.nix new file mode 100644 index 000000000000..3c853973c872 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/mpd.nix @@ -0,0 +1,266 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + name = "mpd"; + + uid = config.ids.uids.mpd; + gid = config.ids.gids.mpd; + cfg = config.services.mpd; + + credentialsPlaceholder = (creds: + let + placeholders = (imap0 + (i: c: ''password "{{password-${toString i}}}@${concatStringsSep "," c.permissions}"'') + creds); + in + concatStringsSep "\n" placeholders); + + mpdConf = pkgs.writeText "mpd.conf" '' + # This file was automatically generated by NixOS. Edit mpd's configuration + # via NixOS' configuration.nix, as this file will be rewritten upon mpd's + # restart. + + music_directory "${cfg.musicDirectory}" + playlist_directory "${cfg.playlistDirectory}" + ${lib.optionalString (cfg.dbFile != null) '' + db_file "${cfg.dbFile}" + ''} + state_file "${cfg.dataDir}/state" + sticker_file "${cfg.dataDir}/sticker.sql" + + ${optionalString (cfg.network.listenAddress != "any") ''bind_to_address "${cfg.network.listenAddress}"''} + ${optionalString (cfg.network.port != 6600) ''port "${toString cfg.network.port}"''} + ${optionalString (cfg.fluidsynth) '' + decoder { + plugin "fluidsynth" + soundfont "${pkgs.soundfont-fluid}/share/soundfonts/FluidR3_GM2-2.sf2" + } + ''} + + ${optionalString (cfg.credentials != []) (credentialsPlaceholder cfg.credentials)} + + ${cfg.extraConfig} + ''; + +in { + + ###### interface + + options = { + + services.mpd = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable MPD, the music player daemon. + ''; + }; + + startWhenNeeded = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + If set, {command}`mpd` is socket-activated; that + is, instead of having it permanently running as a daemon, + systemd will start it on the first incoming connection. + ''; + }; + + musicDirectory = mkOption { + type = with types; either path (strMatching "(http|https|nfs|smb)://.+"); + default = "${cfg.dataDir}/music"; + defaultText = literalExpression ''"''${dataDir}/music"''; + description = lib.mdDoc '' + The directory or NFS/SMB network share where MPD reads music from. If left + as the default value this directory will automatically be created before + the MPD server starts, otherwise the sysadmin is responsible for ensuring + the directory exists with appropriate ownership and permissions. + ''; + }; + + playlistDirectory = mkOption { + type = types.path; + default = "${cfg.dataDir}/playlists"; + defaultText = literalExpression ''"''${dataDir}/playlists"''; + description = lib.mdDoc '' + The directory where MPD stores playlists. If left as the default value + this directory will automatically be created before the MPD server starts, + otherwise the sysadmin is responsible for ensuring the directory exists + with appropriate ownership and permissions. + ''; + }; + + extraConfig = mkOption { + type = types.lines; + default = ""; + description = lib.mdDoc '' + Extra directives added to to the end of MPD's configuration file, + mpd.conf. Basic configuration like file location and uid/gid + is added automatically to the beginning of the file. For available + options see {manpage}`mpd.conf(5)`. + ''; + }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/${name}"; + description = lib.mdDoc '' + The directory where MPD stores its state, tag cache, playlists etc. If + left as the default value this directory will automatically be created + before the MPD server starts, otherwise the sysadmin is responsible for + ensuring the directory exists with appropriate ownership and permissions. + ''; + }; + + user = mkOption { + type = types.str; + default = name; + description = lib.mdDoc "User account under which MPD runs."; + }; + + group = mkOption { + type = types.str; + default = name; + description = lib.mdDoc "Group account under which MPD runs."; + }; + + network = { + + listenAddress = mkOption { + type = types.str; + default = "127.0.0.1"; + example = "any"; + description = lib.mdDoc '' + The address for the daemon to listen on. + Use `any` to listen on all addresses. + ''; + }; + + port = mkOption { + type = types.port; + default = 6600; + description = lib.mdDoc '' + This setting is the TCP port that is desired for the daemon to get assigned + to. + ''; + }; + + }; + + dbFile = mkOption { + type = types.nullOr types.str; + default = "${cfg.dataDir}/tag_cache"; + defaultText = literalExpression ''"''${dataDir}/tag_cache"''; + description = lib.mdDoc '' + The path to MPD's database. If set to `null` the + parameter is omitted from the configuration. + ''; + }; + + credentials = mkOption { + type = types.listOf (types.submodule { + options = { + passwordFile = mkOption { + type = types.path; + description = lib.mdDoc '' + Path to file containing the password. + ''; + }; + permissions = let + perms = ["read" "add" "control" "admin"]; + in mkOption { + type = types.listOf (types.enum perms); + default = [ "read" ]; + description = lib.mdDoc '' + List of permissions that are granted with this password. + Permissions can be "${concatStringsSep "\", \"" perms}". + ''; + }; + }; + }); + description = lib.mdDoc '' + Credentials and permissions for accessing the mpd server. + ''; + default = []; + example = [ + {passwordFile = "/var/lib/secrets/mpd_readonly_password"; permissions = [ "read" ];} + {passwordFile = "/var/lib/secrets/mpd_admin_password"; permissions = ["read" "add" "control" "admin"];} + ]; + }; + + fluidsynth = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + If set, add fluidsynth soundfont and configure the plugin. + ''; + }; + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + # install mpd units + systemd.packages = [ pkgs.mpd ]; + + systemd.sockets.mpd = mkIf cfg.startWhenNeeded { + wantedBy = [ "sockets.target" ]; + listenStreams = [ + "" # Note: this is needed to override the upstream unit + (if pkgs.lib.hasPrefix "/" cfg.network.listenAddress + then cfg.network.listenAddress + else "${optionalString (cfg.network.listenAddress != "any") "${cfg.network.listenAddress}:"}${toString cfg.network.port}") + ]; + }; + + systemd.services.mpd = { + wantedBy = optional (!cfg.startWhenNeeded) "multi-user.target"; + + preStart = + '' + set -euo pipefail + install -m 600 ${mpdConf} /run/mpd/mpd.conf + '' + optionalString (cfg.credentials != []) + (concatStringsSep "\n" + (imap0 + (i: c: ''${pkgs.replace-secret}/bin/replace-secret '{{password-${toString i}}}' '${c.passwordFile}' /run/mpd/mpd.conf'') + cfg.credentials)); + + serviceConfig = + { + User = "${cfg.user}"; + # Note: the first "" overrides the ExecStart from the upstream unit + ExecStart = [ "" "${pkgs.mpd}/bin/mpd --systemd /run/mpd/mpd.conf" ]; + RuntimeDirectory = "mpd"; + StateDirectory = [] + ++ optionals (cfg.dataDir == "/var/lib/${name}") [ name ] + ++ optionals (cfg.playlistDirectory == "/var/lib/${name}/playlists") [ name "${name}/playlists" ] + ++ optionals (cfg.musicDirectory == "/var/lib/${name}/music") [ name "${name}/music" ]; + }; + }; + + users.users = optionalAttrs (cfg.user == name) { + ${name} = { + inherit uid; + group = cfg.group; + extraGroups = [ "audio" ]; + description = "Music Player Daemon user"; + home = "${cfg.dataDir}"; + }; + }; + + users.groups = optionalAttrs (cfg.group == name) { + ${name}.gid = gid; + }; + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/mpdscribble.nix b/nixpkgs/nixos/modules/services/audio/mpdscribble.nix new file mode 100644 index 000000000000..132d9ad32588 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/mpdscribble.nix @@ -0,0 +1,213 @@ +{ config, lib, options, pkgs, ... }: + +with lib; + +let + cfg = config.services.mpdscribble; + mpdCfg = config.services.mpd; + mpdOpt = options.services.mpd; + + endpointUrls = { + "last.fm" = "http://post.audioscrobbler.com"; + "libre.fm" = "http://turtle.libre.fm"; + "jamendo" = "http://postaudioscrobbler.jamendo.com"; + "listenbrainz" = "http://proxy.listenbrainz.org"; + }; + + mkSection = secname: secCfg: '' + [${secname}] + url = ${secCfg.url} + username = ${secCfg.username} + password = {{${secname}_PASSWORD}} + journal = /var/lib/mpdscribble/${secname}.journal + ''; + + endpoints = concatStringsSep "\n" (mapAttrsToList mkSection cfg.endpoints); + cfgTemplate = pkgs.writeText "mpdscribble.conf" '' + ## This file was automatically genenrated by NixOS and will be overwritten. + ## Do not edit. Edit your NixOS configuration instead. + + ## mpdscribble - an audioscrobbler for the Music Player Daemon. + ## http://mpd.wikia.com/wiki/Client:mpdscribble + + # HTTP proxy URL. + ${optionalString (cfg.proxy != null) "proxy = ${cfg.proxy}"} + + # The location of the mpdscribble log file. The special value + # "syslog" makes mpdscribble use the local syslog daemon. On most + # systems, log messages will appear in /var/log/daemon.log then. + # "-" means log to stderr (the current terminal). + log = - + + # How verbose mpdscribble's logging should be. Default is 1. + verbose = ${toString cfg.verbose} + + # How often should mpdscribble save the journal file? [seconds] + journal_interval = ${toString cfg.journalInterval} + + # The host running MPD, possibly protected by a password + # ([PASSWORD@]HOSTNAME). + host = ${(optionalString (cfg.passwordFile != null) "{{MPD_PASSWORD}}@") + cfg.host} + + # The port that the MPD listens on and mpdscribble should try to + # connect to. + port = ${toString cfg.port} + + ${endpoints} + ''; + + cfgFile = "/run/mpdscribble/mpdscribble.conf"; + + replaceSecret = secretFile: placeholder: targetFile: + optionalString (secretFile != null) '' + ${pkgs.replace-secret}/bin/replace-secret '${placeholder}' '${secretFile}' '${targetFile}' ''; + + preStart = pkgs.writeShellScript "mpdscribble-pre-start" '' + cp -f "${cfgTemplate}" "${cfgFile}" + ${replaceSecret cfg.passwordFile "{{MPD_PASSWORD}}" cfgFile} + ${concatStringsSep "\n" (mapAttrsToList (secname: cfg: + replaceSecret cfg.passwordFile "{{${secname}_PASSWORD}}" cfgFile) + cfg.endpoints)} + ''; + + localMpd = (cfg.host == "localhost" || cfg.host == "127.0.0.1"); + +in { + ###### interface + + options.services.mpdscribble = { + + enable = mkEnableOption (lib.mdDoc "mpdscribble"); + + proxy = mkOption { + default = null; + type = types.nullOr types.str; + description = lib.mdDoc '' + HTTP proxy URL. + ''; + }; + + verbose = mkOption { + default = 1; + type = types.int; + description = lib.mdDoc '' + Log level for the mpdscribble daemon. + ''; + }; + + journalInterval = mkOption { + default = 600; + example = 60; + type = types.int; + description = lib.mdDoc '' + How often should mpdscribble save the journal file? [seconds] + ''; + }; + + host = mkOption { + default = (if mpdCfg.network.listenAddress != "any" then + mpdCfg.network.listenAddress + else + "localhost"); + defaultText = literalExpression '' + if config.${mpdOpt.network.listenAddress} != "any" + then config.${mpdOpt.network.listenAddress} + else "localhost" + ''; + type = types.str; + description = lib.mdDoc '' + Host for the mpdscribble daemon to search for a mpd daemon on. + ''; + }; + + passwordFile = mkOption { + default = if localMpd then + (findFirst + (c: any (x: x == "read") c.permissions) + { passwordFile = null; } + mpdCfg.credentials).passwordFile + else + null; + defaultText = literalMD '' + The first password file with read access configured for MPD when using a local instance, + otherwise `null`. + ''; + type = types.nullOr types.str; + description = lib.mdDoc '' + File containing the password for the mpd daemon. + If there is a local mpd configured using {option}`services.mpd.credentials` + the default is automatically set to a matching passwordFile of the local mpd. + ''; + }; + + port = mkOption { + default = mpdCfg.network.port; + defaultText = literalExpression "config.${mpdOpt.network.port}"; + type = types.port; + description = lib.mdDoc '' + Port for the mpdscribble daemon to search for a mpd daemon on. + ''; + }; + + endpoints = mkOption { + type = (let + endpoint = { name, ... }: { + options = { + url = mkOption { + type = types.str; + default = endpointUrls.${name} or ""; + description = + lib.mdDoc "The url endpoint where the scrobble API is listening."; + }; + username = mkOption { + type = types.str; + description = lib.mdDoc '' + Username for the scrobble service. + ''; + }; + passwordFile = mkOption { + type = types.nullOr types.str; + description = + lib.mdDoc "File containing the password, either as MD5SUM or cleartext."; + }; + }; + }; + in types.attrsOf (types.submodule endpoint)); + default = { }; + example = { + "last.fm" = { + username = "foo"; + passwordFile = "/run/secrets/lastfm_password"; + }; + }; + description = lib.mdDoc '' + Endpoints to scrobble to. + If the endpoint is one of "${ + concatStringsSep "\", \"" (attrNames endpointUrls) + }" the url is set automatically. + ''; + }; + + }; + + ###### implementation + + config = mkIf cfg.enable { + systemd.services.mpdscribble = { + after = [ "network.target" ] ++ (optional localMpd "mpd.service"); + description = "mpdscribble mpd scrobble client"; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + DynamicUser = true; + StateDirectory = "mpdscribble"; + RuntimeDirectory = "mpdscribble"; + RuntimeDirectoryMode = "700"; + # TODO use LoadCredential= instead of running preStart with full privileges? + ExecStartPre = "+${preStart}"; + ExecStart = + "${pkgs.mpdscribble}/bin/mpdscribble --no-daemon --conf ${cfgFile}"; + }; + }; + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/mympd.nix b/nixpkgs/nixos/modules/services/audio/mympd.nix new file mode 100644 index 000000000000..f1c7197085d7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/mympd.nix @@ -0,0 +1,129 @@ +{ pkgs, config, lib, ... }: + +let + cfg = config.services.mympd; +in { + options = { + + services.mympd = { + + enable = lib.mkEnableOption (lib.mdDoc "MyMPD server"); + + package = lib.mkPackageOption pkgs "mympd" {}; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = lib.mdDoc '' + Open ports needed for the functionality of the program. + ''; + }; + + extraGroups = lib.mkOption { + type = lib.types.listOf lib.types.str; + default = [ ]; + example = [ "music" ]; + description = lib.mdDoc '' + Additional groups for the systemd service. + ''; + }; + + settings = lib.mkOption { + type = lib.types.submodule { + freeformType = with lib.types; attrsOf (nullOr (oneOf [ str bool int ])); + options = { + http_port = lib.mkOption { + type = lib.types.port; + description = lib.mdDoc '' + The HTTP port where mympd's web interface will be available. + + The HTTPS/SSL port can be configured via {option}`config`. + ''; + example = "8080"; + }; + + ssl = lib.mkOption { + type = lib.types.bool; + description = lib.mdDoc '' + Whether to enable listening on the SSL port. + + Refer to <https://jcorporation.github.io/myMPD/configuration/configuration-files#ssl-options> + for more information. + ''; + default = false; + }; + }; + }; + description = lib.mdDoc '' + Manages the configuration files declaratively. For all the configuration + options, see <https://jcorporation.github.io/myMPD/configuration/configuration-files>. + + Each key represents the "File" column from the upstream configuration table, and the + value is the content of that file. + ''; + }; + }; + + }; + + config = lib.mkIf cfg.enable { + systemd.services.mympd = { + # upstream service config: https://github.com/jcorporation/myMPD/blob/master/contrib/initscripts/mympd.service.in + after = [ "mpd.service" ]; + wantedBy = [ "multi-user.target" ]; + preStart = with lib; '' + config_dir="/var/lib/mympd/config" + mkdir -p "$config_dir" + + ${pipe cfg.settings [ + (mapAttrsToList (name: value: '' + echo -n "${if isBool value then boolToString value else toString value}" > "$config_dir/${name}" + '')) + (concatStringsSep "\n") + ]} + ''; + unitConfig = { + Description = "myMPD server daemon"; + Documentation = "man:mympd(1)"; + }; + serviceConfig = { + AmbientCapabilities = "CAP_NET_BIND_SERVICE"; + CapabilityBoundingSet = "CAP_NET_BIND_SERVICE"; + DynamicUser = true; + ExecStart = lib.getExe cfg.package; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictRealtime = true; + StateDirectory = "mympd"; + CacheDirectory = "mympd"; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_NETLINK AF_UNIX"; + RestrictNamespaces = true; + SystemCallArchitectures = "native"; + SystemCallFilter = "@system-service"; + SupplementaryGroups = cfg.extraGroups; + }; + }; + + networking.firewall = lib.mkMerge [ + (lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.settings.http_port ]; + }) + (lib.mkIf (cfg.openFirewall && cfg.settings.ssl && cfg.settings.ssl_port != null) { + allowedTCPPorts = [ cfg.settings.ssl_port ]; + }) + ]; + + }; + + meta.maintainers = [ lib.maintainers.eliandoran ]; + +} diff --git a/nixpkgs/nixos/modules/services/audio/navidrome.nix b/nixpkgs/nixos/modules/services/audio/navidrome.nix new file mode 100644 index 000000000000..912edb03aa4c --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/navidrome.nix @@ -0,0 +1,84 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.navidrome; + settingsFormat = pkgs.formats.json {}; +in { + options = { + services.navidrome = { + + enable = mkEnableOption (lib.mdDoc "Navidrome music server"); + + package = mkPackageOption pkgs "navidrome" { }; + + settings = mkOption rec { + type = settingsFormat.type; + apply = recursiveUpdate default; + default = { + Address = "127.0.0.1"; + Port = 4533; + }; + example = { + MusicFolder = "/mnt/music"; + }; + description = lib.mdDoc '' + Configuration for Navidrome, see <https://www.navidrome.org/docs/usage/configuration-options/> for supported values. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc "Whether to open the TCP port in the firewall"; + }; + }; + }; + + config = mkIf cfg.enable { + networking.firewall.allowedTCPPorts = mkIf cfg.openFirewall [cfg.settings.Port]; + + systemd.services.navidrome = { + description = "Navidrome Media Server"; + after = [ "network.target" ]; + wantedBy = [ "multi-user.target" ]; + serviceConfig = { + ExecStart = '' + ${cfg.package}/bin/navidrome --configfile ${settingsFormat.generate "navidrome.json" cfg.settings} + ''; + DynamicUser = true; + StateDirectory = "navidrome"; + WorkingDirectory = "/var/lib/navidrome"; + RuntimeDirectory = "navidrome"; + RootDirectory = "/run/navidrome"; + ReadWritePaths = ""; + BindPaths = lib.optional (cfg.settings ? DataFolder) cfg.settings.DataFolder; + BindReadOnlyPaths = [ + # navidrome uses online services to download additional album metadata / covers + "${config.environment.etc."ssl/certs/ca-certificates.crt".source}:/etc/ssl/certs/ca-certificates.crt" + builtins.storeDir + "/etc" + ] ++ lib.optional (cfg.settings ? MusicFolder) cfg.settings.MusicFolder; + CapabilityBoundingSet = ""; + RestrictAddressFamilies = [ "AF_UNIX" "AF_INET" "AF_INET6" ]; + RestrictNamespaces = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ "@system-service" "~@privileged" ]; + RestrictRealtime = true; + LockPersonality = true; + MemoryDenyWriteExecute = true; + UMask = "0066"; + ProtectHostname = true; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/networkaudiod.nix b/nixpkgs/nixos/modules/services/audio/networkaudiod.nix new file mode 100644 index 000000000000..11486429e667 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/networkaudiod.nix @@ -0,0 +1,19 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + name = "networkaudiod"; + cfg = config.services.networkaudiod; +in { + options = { + services.networkaudiod = { + enable = mkEnableOption (lib.mdDoc "Networkaudiod (NAA)"); + }; + }; + + config = mkIf cfg.enable { + systemd.packages = [ pkgs.networkaudiod ]; + systemd.services.networkaudiod.wantedBy = [ "multi-user.target" ]; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/roon-bridge.nix b/nixpkgs/nixos/modules/services/audio/roon-bridge.nix new file mode 100644 index 000000000000..027b0332fd1e --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/roon-bridge.nix @@ -0,0 +1,80 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + name = "roon-bridge"; + cfg = config.services.roon-bridge; +in { + options = { + services.roon-bridge = { + enable = mkEnableOption (lib.mdDoc "Roon Bridge"); + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Open ports in the firewall for the bridge. + ''; + }; + user = mkOption { + type = types.str; + default = "roon-bridge"; + description = lib.mdDoc '' + User to run the Roon bridge as. + ''; + }; + group = mkOption { + type = types.str; + default = "roon-bridge"; + description = lib.mdDoc '' + Group to run the Roon Bridge as. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.roon-bridge = { + after = [ "network.target" ]; + description = "Roon Bridge"; + wantedBy = [ "multi-user.target" ]; + + environment.ROON_DATAROOT = "/var/lib/${name}"; + + serviceConfig = { + ExecStart = "${pkgs.roon-bridge}/bin/RoonBridge"; + LimitNOFILE = 8192; + User = cfg.user; + Group = cfg.group; + StateDirectory = name; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPortRanges = [{ from = 9100; to = 9200; }]; + allowedUDPPorts = [ 9003 ]; + extraCommands = optionalString (!config.networking.nftables.enable) '' + iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT + iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT + iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT + iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT + iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT + ''; + extraInputRules = optionalString config.networking.nftables.enable '' + ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept + ip daddr 224.0.0.0/4 accept + pkttype { multicast, broadcast } accept + ''; + }; + + + users.groups.${cfg.group} = {}; + users.users.${cfg.user} = + optionalAttrs (cfg.user == "roon-bridge") { + isSystemUser = true; + description = "Roon Bridge user"; + group = cfg.group; + extraGroups = [ "audio" ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/roon-server.nix b/nixpkgs/nixos/modules/services/audio/roon-server.nix new file mode 100644 index 000000000000..8691c08b0d36 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/roon-server.nix @@ -0,0 +1,86 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + name = "roon-server"; + cfg = config.services.roon-server; +in { + options = { + services.roon-server = { + enable = mkEnableOption (lib.mdDoc "Roon Server"); + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Open ports in the firewall for the server. + ''; + }; + user = mkOption { + type = types.str; + default = "roon-server"; + description = lib.mdDoc '' + User to run the Roon Server as. + ''; + }; + group = mkOption { + type = types.str; + default = "roon-server"; + description = lib.mdDoc '' + Group to run the Roon Server as. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + systemd.services.roon-server = { + after = [ "network.target" ]; + description = "Roon Server"; + wantedBy = [ "multi-user.target" ]; + + environment.ROON_DATAROOT = "/var/lib/${name}"; + environment.ROON_ID_DIR = "/var/lib/${name}"; + + serviceConfig = { + ExecStart = "${pkgs.roon-server}/bin/RoonServer"; + LimitNOFILE = 8192; + User = cfg.user; + Group = cfg.group; + StateDirectory = name; + }; + }; + + networking.firewall = mkIf cfg.openFirewall { + allowedTCPPortRanges = [ + { from = 9100; to = 9200; } + { from = 9330; to = 9339; } + { from = 30000; to = 30010; } + ]; + allowedUDPPorts = [ 9003 ]; + extraCommands = optionalString (!config.networking.nftables.enable) '' + ## IGMP / Broadcast ## + iptables -A INPUT -s 224.0.0.0/4 -j ACCEPT + iptables -A INPUT -d 224.0.0.0/4 -j ACCEPT + iptables -A INPUT -s 240.0.0.0/5 -j ACCEPT + iptables -A INPUT -m pkttype --pkt-type multicast -j ACCEPT + iptables -A INPUT -m pkttype --pkt-type broadcast -j ACCEPT + ''; + extraInputRules = optionalString config.networking.nftables.enable '' + ip saddr { 224.0.0.0/4, 240.0.0.0/5 } accept + ip daddr 224.0.0.0/4 accept + pkttype { multicast, broadcast } accept + ''; + }; + + + users.groups.${cfg.group} = {}; + users.users.${cfg.user} = + optionalAttrs (cfg.user == "roon-server") { + isSystemUser = true; + description = "Roon Server user"; + group = cfg.group; + extraGroups = [ "audio" ]; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/slimserver.nix b/nixpkgs/nixos/modules/services/audio/slimserver.nix new file mode 100644 index 000000000000..73cda08c5742 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/slimserver.nix @@ -0,0 +1,68 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + + cfg = config.services.slimserver; + +in { + options = { + + services.slimserver = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable slimserver. + ''; + }; + + package = mkPackageOption pkgs "slimserver" { }; + + dataDir = mkOption { + type = types.path; + default = "/var/lib/slimserver"; + description = lib.mdDoc '' + The directory where slimserver stores its state, tag cache, + playlists etc. + ''; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + + systemd.tmpfiles.rules = [ + "d '${cfg.dataDir}' - slimserver slimserver - -" + ]; + + systemd.services.slimserver = { + after = [ "network.target" ]; + description = "Slim Server for Logitech Squeezebox Players"; + wantedBy = [ "multi-user.target" ]; + + serviceConfig = { + User = "slimserver"; + # Issue 40589: Disable broken image/video support (audio still works!) + ExecStart = "${lib.getExe cfg.package} --logdir ${cfg.dataDir}/logs --prefsdir ${cfg.dataDir}/prefs --cachedir ${cfg.dataDir}/cache --noimage --novideo"; + }; + }; + + users = { + users.slimserver = { + description = "Slimserver daemon user"; + home = cfg.dataDir; + group = "slimserver"; + isSystemUser = true; + }; + groups.slimserver = {}; + }; + }; + +} + diff --git a/nixpkgs/nixos/modules/services/audio/snapserver.nix b/nixpkgs/nixos/modules/services/audio/snapserver.nix new file mode 100644 index 000000000000..dbab741bf6fc --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/snapserver.nix @@ -0,0 +1,316 @@ +{ config, options, lib, pkgs, ... }: + +with lib; + +let + + name = "snapserver"; + + cfg = config.services.snapserver; + + # Using types.nullOr to inherit upstream defaults. + sampleFormat = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Default sample format. + ''; + example = "48000:16:2"; + }; + + codec = mkOption { + type = with types; nullOr str; + default = null; + description = lib.mdDoc '' + Default audio compression method. + ''; + example = "flac"; + }; + + streamToOption = name: opt: + let + os = val: + optionalString (val != null) "${val}"; + os' = prefix: val: + optionalString (val != null) (prefix + "${val}"); + flatten = key: value: + "&${key}=${value}"; + in + "--stream.stream=\"${opt.type}://" + os opt.location + "?" + os' "name=" name + + concatStrings (mapAttrsToList flatten opt.query) + "\""; + + optionalNull = val: ret: + optional (val != null) ret; + + optionString = concatStringsSep " " (mapAttrsToList streamToOption cfg.streams + # global options + ++ [ "--stream.bind_to_address=${cfg.listenAddress}" ] + ++ [ "--stream.port=${toString cfg.port}" ] + ++ optionalNull cfg.sampleFormat "--stream.sampleformat=${cfg.sampleFormat}" + ++ optionalNull cfg.codec "--stream.codec=${cfg.codec}" + ++ optionalNull cfg.streamBuffer "--stream.stream_buffer=${toString cfg.streamBuffer}" + ++ optionalNull cfg.buffer "--stream.buffer=${toString cfg.buffer}" + ++ optional cfg.sendToMuted "--stream.send_to_muted" + # tcp json rpc + ++ [ "--tcp.enabled=${toString cfg.tcp.enable}" ] + ++ optionals cfg.tcp.enable [ + "--tcp.bind_to_address=${cfg.tcp.listenAddress}" + "--tcp.port=${toString cfg.tcp.port}" ] + # http json rpc + ++ [ "--http.enabled=${toString cfg.http.enable}" ] + ++ optionals cfg.http.enable [ + "--http.bind_to_address=${cfg.http.listenAddress}" + "--http.port=${toString cfg.http.port}" + ] ++ optional (cfg.http.docRoot != null) "--http.doc_root=\"${toString cfg.http.docRoot}\""); + +in { + imports = [ + (mkRenamedOptionModule [ "services" "snapserver" "controlPort" ] [ "services" "snapserver" "tcp" "port" ]) + ]; + + ###### interface + + options = { + + services.snapserver = { + + enable = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to enable snapserver. + ''; + }; + + listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = lib.mdDoc '' + The address where snapclients can connect. + ''; + }; + + port = mkOption { + type = types.port; + default = 1704; + description = lib.mdDoc '' + The port that snapclients can connect to. + ''; + }; + + openFirewall = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Whether to automatically open the specified ports in the firewall. + ''; + }; + + inherit sampleFormat; + inherit codec; + + streamBuffer = mkOption { + type = with types; nullOr int; + default = null; + description = lib.mdDoc '' + Stream read (input) buffer in ms. + ''; + example = 20; + }; + + buffer = mkOption { + type = with types; nullOr int; + default = null; + description = lib.mdDoc '' + Network buffer in ms. + ''; + example = 1000; + }; + + sendToMuted = mkOption { + type = types.bool; + default = false; + description = lib.mdDoc '' + Send audio to muted clients. + ''; + }; + + tcp.enable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable the JSON-RPC via TCP. + ''; + }; + + tcp.listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = lib.mdDoc '' + The address where the TCP JSON-RPC listens on. + ''; + }; + + tcp.port = mkOption { + type = types.port; + default = 1705; + description = lib.mdDoc '' + The port where the TCP JSON-RPC listens on. + ''; + }; + + http.enable = mkOption { + type = types.bool; + default = true; + description = lib.mdDoc '' + Whether to enable the JSON-RPC via HTTP. + ''; + }; + + http.listenAddress = mkOption { + type = types.str; + default = "::"; + example = "0.0.0.0"; + description = lib.mdDoc '' + The address where the HTTP JSON-RPC listens on. + ''; + }; + + http.port = mkOption { + type = types.port; + default = 1780; + description = lib.mdDoc '' + The port where the HTTP JSON-RPC listens on. + ''; + }; + + http.docRoot = mkOption { + type = with types; nullOr path; + default = null; + description = lib.mdDoc '' + Path to serve from the HTTP servers root. + ''; + }; + + streams = mkOption { + type = with types; attrsOf (submodule { + options = { + location = mkOption { + type = types.oneOf [ types.path types.str ]; + description = lib.mdDoc '' + For type `pipe` or `file`, the path to the pipe or file. + For type `librespot`, `airplay` or `process`, the path to the corresponding binary. + For type `tcp`, the `host:port` address to connect to or listen on. + For type `meta`, a list of stream names in the form `/one/two/...`. Don't forget the leading slash. + For type `alsa`, use an empty string. + ''; + example = literalExpression '' + "/path/to/pipe" + "/path/to/librespot" + "192.168.1.2:4444" + "/MyTCP/Spotify/MyPipe" + ''; + }; + type = mkOption { + type = types.enum [ "pipe" "librespot" "airplay" "file" "process" "tcp" "alsa" "spotify" "meta" ]; + default = "pipe"; + description = lib.mdDoc '' + The type of input stream. + ''; + }; + query = mkOption { + type = attrsOf str; + default = {}; + description = lib.mdDoc '' + Key-value pairs that convey additional parameters about a stream. + ''; + example = literalExpression '' + # for type == "pipe": + { + mode = "create"; + }; + # for type == "process": + { + params = "--param1 --param2"; + logStderr = "true"; + }; + # for type == "tcp": + { + mode = "client"; + } + # for type == "alsa": + { + device = "hw:0,0"; + } + ''; + }; + inherit sampleFormat; + inherit codec; + }; + }); + default = { default = {}; }; + description = lib.mdDoc '' + The definition for an input source. + ''; + example = literalExpression '' + { + mpd = { + type = "pipe"; + location = "/run/snapserver/mpd"; + sampleFormat = "48000:16:2"; + codec = "pcm"; + }; + }; + ''; + }; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + + warnings = + # https://github.com/badaix/snapcast/blob/98ac8b2fb7305084376607b59173ce4097c620d8/server/streamreader/stream_manager.cpp#L85 + filter (w: w != "") (mapAttrsToList (k: v: optionalString (v.type == "spotify") '' + services.snapserver.streams.${k}.type = "spotify" is deprecated, use services.snapserver.streams.${k}.type = "librespot" instead. + '') cfg.streams); + + systemd.services.snapserver = { + after = [ "network.target" ]; + description = "Snapserver"; + wantedBy = [ "multi-user.target" ]; + before = [ "mpd.service" "mopidy.service" ]; + + serviceConfig = { + DynamicUser = true; + ExecStart = "${pkgs.snapcast}/bin/snapserver --daemon ${optionString}"; + Type = "forking"; + LimitRTPRIO = 50; + LimitRTTIME = "infinity"; + NoNewPrivileges = true; + PIDFile = "/run/${name}/pid"; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectKernelModules = true; + RestrictAddressFamilies = "AF_INET AF_INET6 AF_UNIX AF_NETLINK"; + RestrictNamespaces = true; + RuntimeDirectory = name; + StateDirectory = name; + }; + }; + + networking.firewall.allowedTCPPorts = + optionals cfg.openFirewall [ cfg.port ] + ++ optional (cfg.openFirewall && cfg.tcp.enable) cfg.tcp.port + ++ optional (cfg.openFirewall && cfg.http.enable) cfg.http.port; + }; + + meta = { + maintainers = with maintainers; [ tobim ]; + }; + +} diff --git a/nixpkgs/nixos/modules/services/audio/spotifyd.nix b/nixpkgs/nixos/modules/services/audio/spotifyd.nix new file mode 100644 index 000000000000..1194b6f200d7 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/spotifyd.nix @@ -0,0 +1,69 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.spotifyd; + toml = pkgs.formats.toml {}; + warnConfig = + if cfg.config != "" + then lib.trace "Using the stringly typed .config attribute is discouraged. Use the TOML typed .settings attribute instead." + else id; + spotifydConf = + if cfg.settings != {} + then toml.generate "spotify.conf" cfg.settings + else warnConfig (pkgs.writeText "spotifyd.conf" cfg.config); +in +{ + options = { + services.spotifyd = { + enable = mkEnableOption (lib.mdDoc "spotifyd, a Spotify playing daemon"); + + config = mkOption { + default = ""; + type = types.lines; + description = lib.mdDoc '' + (Deprecated) Configuration for Spotifyd. For syntax and directives, see + <https://github.com/Spotifyd/spotifyd#Configuration>. + ''; + }; + + settings = mkOption { + default = {}; + type = toml.type; + example = { global.bitrate = 320; }; + description = lib.mdDoc '' + Configuration for Spotifyd. For syntax and directives, see + <https://github.com/Spotifyd/spotifyd#Configuration>. + ''; + }; + }; + }; + + config = mkIf cfg.enable { + assertions = [ + { + assertion = cfg.config == "" || cfg.settings == {}; + message = "At most one of the .config attribute and the .settings attribute may be set"; + } + ]; + + systemd.services.spotifyd = { + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" "sound.target" ]; + description = "spotifyd, a Spotify playing daemon"; + environment.SHELL = "/bin/sh"; + serviceConfig = { + ExecStart = "${pkgs.spotifyd}/bin/spotifyd --no-daemon --cache-path /var/cache/spotifyd --config-path ${spotifydConf}"; + Restart = "always"; + RestartSec = 12; + DynamicUser = true; + CacheDirectory = "spotifyd"; + SupplementaryGroups = ["audio"]; + }; + }; + }; + + meta.maintainers = [ maintainers.anderslundstedt ]; +} diff --git a/nixpkgs/nixos/modules/services/audio/squeezelite.nix b/nixpkgs/nixos/modules/services/audio/squeezelite.nix new file mode 100644 index 000000000000..30dc12552f00 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/squeezelite.nix @@ -0,0 +1,46 @@ +{ config, lib, pkgs, ... }: + +let + inherit (lib) mkEnableOption mkIf mkOption optionalString types; + + dataDir = "/var/lib/squeezelite"; + cfg = config.services.squeezelite; + pkg = if cfg.pulseAudio then pkgs.squeezelite-pulse else pkgs.squeezelite; + bin = "${pkg}/bin/${pkg.pname}"; + +in +{ + + ###### interface + + options.services.squeezelite = { + enable = mkEnableOption (lib.mdDoc "Squeezelite, a software Squeezebox emulator"); + + pulseAudio = mkEnableOption (lib.mdDoc "pulseaudio support"); + + extraArguments = mkOption { + default = ""; + type = types.str; + description = lib.mdDoc '' + Additional command line arguments to pass to Squeezelite. + ''; + }; + }; + + + ###### implementation + + config = mkIf cfg.enable { + systemd.services.squeezelite = { + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" "sound.target" ]; + description = "Software Squeezebox emulator"; + serviceConfig = { + DynamicUser = true; + ExecStart = "${bin} -N ${dataDir}/player-name ${cfg.extraArguments}"; + StateDirectory = builtins.baseNameOf dataDir; + SupplementaryGroups = "audio"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/tts.nix b/nixpkgs/nixos/modules/services/audio/tts.nix new file mode 100644 index 000000000000..0d93224ec030 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/tts.nix @@ -0,0 +1,152 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.tts; +in + +{ + options.services.tts = let + inherit (lib) literalExpression mkOption mdDoc mkEnableOption types; + in { + servers = mkOption { + type = types.attrsOf (types.submodule ( + { ... }: { + options = { + enable = mkEnableOption (mdDoc "Coqui TTS server"); + + port = mkOption { + type = types.port; + example = 5000; + description = mdDoc '' + Port to bind the TTS server to. + ''; + }; + + model = mkOption { + type = types.nullOr types.str; + default = "tts_models/en/ljspeech/tacotron2-DDC"; + example = null; + description = mdDoc '' + Name of the model to download and use for speech synthesis. + + Check `tts-server --list_models` for possible values. + + Set to `null` to use a custom model. + ''; + }; + + useCuda = mkOption { + type = types.bool; + default = false; + example = true; + description = mdDoc '' + Whether to offload computation onto a CUDA compatible GPU. + ''; + }; + + extraArgs = mkOption { + type = types.listOf types.str; + default = []; + description = mdDoc '' + Extra arguments to pass to the server commandline. + ''; + }; + }; + } + )); + default = {}; + example = literalExpression '' + { + english = { + port = 5300; + model = "tts_models/en/ljspeech/tacotron2-DDC"; + }; + german = { + port = 5301; + model = "tts_models/de/thorsten/tacotron2-DDC"; + }; + dutch = { + port = 5302; + model = "tts_models/nl/mai/tacotron2-DDC"; + }; + } + ''; + description = mdDoc '' + TTS server instances. + ''; + }; + }; + + config = let + inherit (lib) mkIf mapAttrs' nameValuePair optionalString concatMapStringsSep escapeShellArgs; + in mkIf (cfg.servers != {}) { + systemd.services = mapAttrs' (server: options: + nameValuePair "tts-${server}" { + description = "Coqui TTS server instance ${server}"; + after = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + path = with pkgs; [ + espeak-ng + ]; + environment.HOME = "/var/lib/tts"; + serviceConfig = { + DynamicUser = true; + User = "tts"; + StateDirectory = "tts"; + ExecStart = "${pkgs.tts}/bin/tts-server --port ${toString options.port}" + + optionalString (options.model != null) " --model_name ${options.model}" + + optionalString (options.useCuda) " --use_cuda" + + (concatMapStringsSep " " escapeShellArgs options.extraArgs); + CapabilityBoundingSet = ""; + DeviceAllow = if options.useCuda then [ + # https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf + "/dev/nvidia1" + "/dev/nvidia2" + "/dev/nvidia3" + "/dev/nvidia4" + "/dev/nvidia-caps/nvidia-cap1" + "/dev/nvidia-caps/nvidia-cap2" + "/dev/nvidiactl" + "/dev/nvidia-modeset" + "/dev/nvidia-uvm" + "/dev/nvidia-uvm-tools" + ] else ""; + DevicePolicy = "closed"; + LockPersonality = true; + # jit via numba->llvmpipe + MemoryDenyWriteExecute = false; + PrivateDevices = true; + PrivateUsers = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = [ + "AF_UNIX" + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }) cfg.servers; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/wyoming/faster-whisper.nix b/nixpkgs/nixos/modules/services/audio/wyoming/faster-whisper.nix new file mode 100644 index 000000000000..dd7f62744cd0 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/wyoming/faster-whisper.nix @@ -0,0 +1,191 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.wyoming.faster-whisper; + + inherit (lib) + escapeShellArgs + mkOption + mdDoc + mkEnableOption + mkPackageOption + types + ; + + inherit (builtins) + toString + ; + +in + +{ + options.services.wyoming.faster-whisper = with types; { + package = mkPackageOption pkgs "wyoming-faster-whisper" { }; + + servers = mkOption { + default = {}; + description = mdDoc '' + Attribute set of faster-whisper instances to spawn. + ''; + type = types.attrsOf (types.submodule ( + { ... }: { + options = { + enable = mkEnableOption (mdDoc "Wyoming faster-whisper server"); + + model = mkOption { + # Intersection between available and referenced models here: + # https://github.com/rhasspy/models/releases/tag/v1.0 + # https://github.com/rhasspy/rhasspy3/blob/wyoming-v1/programs/asr/faster-whisper/server/wyoming_faster_whisper/download.py#L17-L27 + type = enum [ + "tiny" + "tiny-int8" + "base" + "base-int8" + "small" + "small-int8" + "medium-int8" + ]; + default = "tiny-int8"; + example = "medium-int8"; + description = mdDoc '' + Name of the voice model to use. + ''; + }; + + uri = mkOption { + type = strMatching "^(tcp|unix)://.*$"; + example = "tcp://0.0.0.0:10300"; + description = mdDoc '' + URI to bind the wyoming server to. + ''; + }; + + device = mkOption { + # https://opennmt.net/CTranslate2/python/ctranslate2.models.Whisper.html# + type = types.enum [ + "cpu" + "cuda" + "auto" + ]; + default = "cpu"; + description = mdDoc '' + Determines the platform faster-whisper is run on. CPU works everywhere, CUDA requires a compatible NVIDIA GPU. + ''; + }; + + language = mkOption { + type = enum [ + # https://github.com/home-assistant/addons/blob/master/whisper/config.yaml#L20 + "auto" "af" "am" "ar" "as" "az" "ba" "be" "bg" "bn" "bo" "br" "bs" "ca" "cs" "cy" "da" "de" "el" "en" "es" "et" "eu" "fa" "fi" "fo" "fr" "gl" "gu" "ha" "haw" "he" "hi" "hr" "ht" "hu" "hy" "id" "is" "it" "ja" "jw" "ka" "kk" "km" "kn" "ko" "la" "lb" "ln" "lo" "lt" "lv" "mg" "mi" "mk" "ml" "mn" "mr" "ms" "mt" "my" "ne" "nl" "nn" "no" "oc" "pa" "pl" "ps" "pt" "ro" "ru" "sa" "sd" "si" "sk" "sl" "sn" "so" "sq" "sr" "su" "sv" "sw" "ta" "te" "tg" "th" "tk" "tl" "tr" "tt" "uk" "ur" "uz" "vi" "yi" "yo" "zh" + ]; + example = "en"; + description = mdDoc '' + The language used to to parse words and sentences. + ''; + }; + + beamSize = mkOption { + type = ints.unsigned; + default = 1; + example = 5; + description = mdDoc '' + The number of beams to use in beam search. + ''; + apply = toString; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = mdDoc '' + Extra arguments to pass to the server commandline. + ''; + apply = escapeShellArgs; + }; + }; + } + )); + }; + }; + + config = let + inherit (lib) + mapAttrs' + mkIf + nameValuePair + ; + in mkIf (cfg.servers != {}) { + systemd.services = mapAttrs' (server: options: + nameValuePair "wyoming-faster-whisper-${server}" { + inherit (options) enable; + description = "Wyoming faster-whisper server instance ${server}"; + after = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + serviceConfig = { + DynamicUser = true; + User = "wyoming-faster-whisper"; + StateDirectory = "wyoming/faster-whisper"; + # https://github.com/home-assistant/addons/blob/master/whisper/rootfs/etc/s6-overlay/s6-rc.d/whisper/run + ExecStart = '' + ${cfg.package}/bin/wyoming-faster-whisper \ + --data-dir $STATE_DIRECTORY \ + --download-dir $STATE_DIRECTORY \ + --uri ${options.uri} \ + --device ${options.device} \ + --model ${options.model} \ + --language ${options.language} \ + --beam-size ${options.beamSize} ${options.extraArgs} + ''; + CapabilityBoundingSet = ""; + DeviceAllow = if builtins.elem options.device [ "cuda" "auto" ] then [ + # https://docs.nvidia.com/dgx/pdf/dgx-os-5-user-guide.pdf + # CUDA not working? Check DeviceAllow and PrivateDevices first! + "/dev/nvidia0" + "/dev/nvidia1" + "/dev/nvidia2" + "/dev/nvidia3" + "/dev/nvidia4" + "/dev/nvidia-caps/nvidia-cap1" + "/dev/nvidia-caps/nvidia-cap2" + "/dev/nvidiactl" + "/dev/nvidia-modeset" + "/dev/nvidia-uvm" + "/dev/nvidia-uvm-tools" + ] else ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateUsers = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }) cfg.servers; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/wyoming/openwakeword.nix b/nixpkgs/nixos/modules/services/audio/wyoming/openwakeword.nix new file mode 100644 index 000000000000..252f70be2baa --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/wyoming/openwakeword.nix @@ -0,0 +1,163 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.wyoming.openwakeword; + + inherit (lib) + concatStringsSep + concatMapStringsSep + escapeShellArgs + mkOption + mdDoc + mkEnableOption + mkIf + mkPackageOption + mkRemovedOptionModule + types + ; + + inherit (builtins) + toString + ; + +in + +{ + imports = [ + (mkRemovedOptionModule [ "services" "wyoming" "openwakeword" "models" ] "Configuring models has been removed, they are now dynamically discovered and loaded at runtime") + ]; + + meta.buildDocsInSandbox = false; + + options.services.wyoming.openwakeword = with types; { + enable = mkEnableOption (mdDoc "Wyoming openWakeWord server"); + + package = mkPackageOption pkgs "wyoming-openwakeword" { }; + + uri = mkOption { + type = strMatching "^(tcp|unix)://.*$"; + default = "tcp://0.0.0.0:10400"; + example = "tcp://192.0.2.1:5000"; + description = mdDoc '' + URI to bind the wyoming server to. + ''; + }; + + customModelsDirectories = mkOption { + type = listOf types.path; + default = []; + description = lib.mdDoc '' + Paths to directories with custom wake word models (*.tflite model files). + ''; + }; + + preloadModels = mkOption { + type = listOf str; + default = [ + "ok_nabu" + ]; + example = [ + # wyoming_openwakeword/models/*.tflite + "alexa" + "hey_jarvis" + "hey_mycroft" + "hey_rhasspy" + "ok_nabu" + ]; + description = mdDoc '' + List of wake word models to preload after startup. + ''; + }; + + threshold = mkOption { + type = float; + default = 0.5; + description = mdDoc '' + Activation threshold (0-1), where higher means fewer activations. + + See trigger level for the relationship between activations and + wake word detections. + ''; + apply = toString; + }; + + triggerLevel = mkOption { + type = int; + default = 1; + description = mdDoc '' + Number of activations before a detection is registered. + + A higher trigger level means fewer detections. + ''; + apply = toString; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = mdDoc '' + Extra arguments to pass to the server commandline. + ''; + apply = escapeShellArgs; + }; + }; + + config = mkIf cfg.enable { + systemd.services."wyoming-openwakeword" = { + description = "Wyoming openWakeWord server"; + after = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + serviceConfig = { + DynamicUser = true; + User = "wyoming-openwakeword"; + # https://github.com/home-assistant/addons/blob/master/openwakeword/rootfs/etc/s6-overlay/s6-rc.d/openwakeword/run + ExecStart = concatStringsSep " " [ + "${cfg.package}/bin/wyoming-openwakeword" + "--uri ${cfg.uri}" + (concatMapStringsSep " " (model: "--preload-model ${model}") cfg.preloadModels) + (concatMapStringsSep " " (dir: "--custom-model-dir ${toString dir}") cfg.customModelsDirectories) + "--threshold ${cfg.threshold}" + "--trigger-level ${cfg.triggerLevel}" + "${cfg.extraArgs}" + ]; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "all"; # reads /proc/cpuinfo + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RuntimeDirectory = "wyoming-openwakeword"; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/wyoming/piper.nix b/nixpkgs/nixos/modules/services/audio/wyoming/piper.nix new file mode 100644 index 000000000000..2828fdf07892 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/wyoming/piper.nix @@ -0,0 +1,175 @@ +{ config +, lib +, pkgs +, ... +}: + +let + cfg = config.services.wyoming.piper; + + inherit (lib) + escapeShellArgs + mkOption + mdDoc + mkEnableOption + mkPackageOption + types + ; + + inherit (builtins) + toString + ; + +in + +{ + meta.buildDocsInSandbox = false; + + options.services.wyoming.piper = with types; { + package = mkPackageOption pkgs "wyoming-piper" { }; + + servers = mkOption { + default = {}; + description = mdDoc '' + Attribute set of piper instances to spawn. + ''; + type = types.attrsOf (types.submodule ( + { ... }: { + options = { + enable = mkEnableOption (mdDoc "Wyoming Piper server"); + + piper = mkPackageOption pkgs "piper-tts" { }; + + voice = mkOption { + type = str; + example = "en-us-ryan-medium"; + description = mdDoc '' + Name of the voice model to use. See the following website for samples: + https://rhasspy.github.io/piper-samples/ + ''; + }; + + uri = mkOption { + type = strMatching "^(tcp|unix)://.*$"; + example = "tcp://0.0.0.0:10200"; + description = mdDoc '' + URI to bind the wyoming server to. + ''; + }; + + speaker = mkOption { + type = ints.unsigned; + default = 0; + description = mdDoc '' + ID of a specific speaker in a multi-speaker model. + ''; + apply = toString; + }; + + noiseScale = mkOption { + type = float; + default = 0.667; + description = mdDoc '' + Generator noise value. + ''; + apply = toString; + }; + + noiseWidth = mkOption { + type = float; + default = 0.333; + description = mdDoc '' + Phoneme width noise value. + ''; + apply = toString; + }; + + lengthScale = mkOption { + type = float; + default = 1.0; + description = mdDoc '' + Phoneme length value. + ''; + apply = toString; + }; + + extraArgs = mkOption { + type = listOf str; + default = [ ]; + description = mdDoc '' + Extra arguments to pass to the server commandline. + ''; + apply = escapeShellArgs; + }; + }; + } + )); + }; + }; + + config = let + inherit (lib) + mapAttrs' + mkIf + nameValuePair + ; + in mkIf (cfg.servers != {}) { + systemd.services = mapAttrs' (server: options: + nameValuePair "wyoming-piper-${server}" { + inherit (options) enable; + description = "Wyoming Piper server instance ${server}"; + after = [ + "network-online.target" + ]; + wantedBy = [ + "multi-user.target" + ]; + serviceConfig = { + DynamicUser = true; + User = "wyoming-piper"; + StateDirectory = "wyoming/piper"; + # https://github.com/home-assistant/addons/blob/master/piper/rootfs/etc/s6-overlay/s6-rc.d/piper/run + ExecStart = '' + ${cfg.package}/bin/wyoming-piper \ + --data-dir $STATE_DIRECTORY \ + --download-dir $STATE_DIRECTORY \ + --uri ${options.uri} \ + --piper ${options.piper}/bin/piper \ + --voice ${options.voice} \ + --speaker ${options.speaker} \ + --length-scale ${options.lengthScale} \ + --noise-scale ${options.noiseScale} \ + --noise-w ${options.noiseWidth} ${options.extraArgs} + ''; + CapabilityBoundingSet = ""; + DeviceAllow = ""; + DevicePolicy = "closed"; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectControlGroups = true; + ProtectProc = "invisible"; + ProcSubset = "pid"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + "AF_UNIX" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + ]; + UMask = "0077"; + }; + }) cfg.servers; + }; +} diff --git a/nixpkgs/nixos/modules/services/audio/ympd.nix b/nixpkgs/nixos/modules/services/audio/ympd.nix new file mode 100644 index 000000000000..6e8d22dab3c8 --- /dev/null +++ b/nixpkgs/nixos/modules/services/audio/ympd.nix @@ -0,0 +1,96 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + cfg = config.services.ympd; +in { + + ###### interface + + options = { + + services.ympd = { + + enable = mkEnableOption (lib.mdDoc "ympd, the MPD Web GUI"); + + webPort = mkOption { + type = types.either types.str types.port; # string for backwards compat + default = "8080"; + description = lib.mdDoc "The port where ympd's web interface will be available."; + example = "ssl://8080:/path/to/ssl-private-key.pem"; + }; + + mpd = { + host = mkOption { + type = types.str; + default = "localhost"; + description = lib.mdDoc "The host where MPD is listening."; + }; + + port = mkOption { + type = types.port; + default = config.services.mpd.network.port; + defaultText = literalExpression "config.services.mpd.network.port"; + description = lib.mdDoc "The port where MPD is listening."; + example = 6600; + }; + }; + + }; + + }; + + + ###### implementation + + config = mkIf cfg.enable { + + systemd.services.ympd = { + description = "Standalone MPD Web GUI written in C"; + + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + + serviceConfig = { + ExecStart = '' + ${pkgs.ympd}/bin/ympd \ + --host ${cfg.mpd.host} \ + --port ${toString cfg.mpd.port} \ + --webport ${toString cfg.webPort} + ''; + + DynamicUser = true; + NoNewPrivileges = true; + + ProtectProc = "invisible"; + ProtectSystem = "strict"; + ProtectHome = "tmpfs"; + + PrivateTmp = true; + PrivateDevices = true; + PrivateIPC = true; + + ProtectHostname = true; + ProtectClock = true; + ProtectKernelTunables = true; + ProtectKernelModules = true; + ProtectKernelLogs = true; + ProtectControlGroups = true; + + RestrictAddressFamilies = [ "AF_INET" "AF_INET6" ]; + RestrictRealtime = true; + RestrictSUIDSGID = true; + + SystemCallFilter = [ + "@system-service" + "~@process" + "~@setuid" + ]; + }; + }; + + }; + +} |