diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix index 2313f45b8e5eb..3eece09f1aed5 100644 --- a/nixos/modules/module-list.nix +++ b/nixos/modules/module-list.nix @@ -183,6 +183,7 @@ ./services/amqp/activemq/default.nix ./services/amqp/rabbitmq.nix ./services/audio/alsa.nix + ./services/audio/jack.nix ./services/audio/icecast.nix ./services/audio/liquidsoap.nix ./services/audio/mpd.nix diff --git a/nixos/modules/services/audio/jack.nix b/nixos/modules/services/audio/jack.nix new file mode 100644 index 0000000000000..1364abd40447d --- /dev/null +++ b/nixos/modules/services/audio/jack.nix @@ -0,0 +1,275 @@ +{ 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.alsaLib != 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 '' + JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group + ''; + + package = mkOption { + # until jack1 promiscuous mode is fixed + internal = true; + type = types.package; + default = pkgs.jack2; + defaultText = "pkgs.jack2"; + example = literalExample "pkgs.jack1"; + description = '' + The JACK package to use. + ''; + }; + + extraOptions = mkOption { + type = types.listOf types.str; + default = [ + "-dalsa" + ]; + example = literalExample '' + [ "-dalsa" "--device" "hw:1" ]; + ''; + description = '' + Specifies startup command line arguments to pass to JACK server. + ''; + }; + + session = mkOption { + type = types.lines; + description = '' + Commands to run after JACK is started. + ''; + }; + + }; + + alsa = { + enable = mkOption { + type = types.bool; + default = true; + description = '' + Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin. + ''; + }; + + support32Bit = mkOption { + type = types.bool; + default = false; + description = '' + Whether to support sound for 32-bit ALSA applications on 64-bit system. + ''; + }; + }; + + loopback = { + enable = mkOption { + type = types.bool; + default = false; + description = '' + 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 = '' + Index of an ALSA loopback device. + ''; + }; + + config = mkOption { + type = types.lines; + description = '' + ALSA config for loopback device. + ''; + }; + + session = mkOption { + type = types.lines; + description = '' + Additional commands to run to setup loopback device. + ''; + }; + }; + + }; + + }; + + config = mkMerge [ + + (mkIf pcmPlugin { + sound.extraConfig = '' + pcm_type.jack { + libs.native = ${pkgs.alsaPlugins}/lib/alsa-lib/libasound_module_pcm_jack.so ; + ${lib.optionalString enable32BitAlsaPlugins + "libs.32Bit = ${pkgs.pkgsi686Linux.alsaPlugins}/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 + } + } + 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"; + }; + # http://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.alsaPlugins}/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"; + 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 = [ maintainers.gnidorah ]; +}