1{ config, lib, pkgs, ... }:
2
3with lib;
4
5let
6 cfg = config.services.jack;
7
8 pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
9 loopback = cfg.jackd.enable && cfg.loopback.enable;
10
11 enable32BitAlsaPlugins = cfg.alsa.support32Bit && pkgs.stdenv.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
12
13 umaskNeeded = versionOlder cfg.jackd.package.version "1.9.12";
14 bridgeNeeded = versionAtLeast cfg.jackd.package.version "1.9.12";
15in {
16 options = {
17 services.jack = {
18 jackd = {
19 enable = mkEnableOption (lib.mdDoc ''
20 JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group
21 '');
22
23 package = mkOption {
24 # until jack1 promiscuous mode is fixed
25 internal = true;
26 type = types.package;
27 default = pkgs.jack2;
28 defaultText = literalExpression "pkgs.jack2";
29 example = literalExpression "pkgs.jack1";
30 description = lib.mdDoc ''
31 The JACK package to use.
32 '';
33 };
34
35 extraOptions = mkOption {
36 type = types.listOf types.str;
37 default = [
38 "-dalsa"
39 ];
40 example = literalExpression ''
41 [ "-dalsa" "--device" "hw:1" ];
42 '';
43 description = lib.mdDoc ''
44 Specifies startup command line arguments to pass to JACK server.
45 '';
46 };
47
48 session = mkOption {
49 type = types.lines;
50 description = lib.mdDoc ''
51 Commands to run after JACK is started.
52 '';
53 };
54
55 };
56
57 alsa = {
58 enable = mkOption {
59 type = types.bool;
60 default = true;
61 description = lib.mdDoc ''
62 Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin.
63 '';
64 };
65
66 support32Bit = mkOption {
67 type = types.bool;
68 default = false;
69 description = lib.mdDoc ''
70 Whether to support sound for 32-bit ALSA applications on 64-bit system.
71 '';
72 };
73 };
74
75 loopback = {
76 enable = mkOption {
77 type = types.bool;
78 default = false;
79 description = lib.mdDoc ''
80 Create ALSA loopback device, instead of using PCM plugin. Has broader
81 application support (things like Steam will work), but may need fine-tuning
82 for concrete hardware.
83 '';
84 };
85
86 index = mkOption {
87 type = types.int;
88 default = 10;
89 description = lib.mdDoc ''
90 Index of an ALSA loopback device.
91 '';
92 };
93
94 config = mkOption {
95 type = types.lines;
96 description = lib.mdDoc ''
97 ALSA config for loopback device.
98 '';
99 };
100
101 dmixConfig = mkOption {
102 type = types.lines;
103 default = "";
104 example = ''
105 period_size 2048
106 periods 2
107 '';
108 description = lib.mdDoc ''
109 For music production software that still doesn't support JACK natively you
110 would like to put buffer/period adjustments here
111 to decrease dmix device latency.
112 '';
113 };
114
115 session = mkOption {
116 type = types.lines;
117 description = lib.mdDoc ''
118 Additional commands to run to setup loopback device.
119 '';
120 };
121 };
122
123 };
124
125 };
126
127 config = mkMerge [
128
129 (mkIf pcmPlugin {
130 sound.extraConfig = ''
131 pcm_type.jack {
132 libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
133 ${lib.optionalString enable32BitAlsaPlugins
134 "libs.32Bit = ${pkgs.pkgsi686Linux.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;"}
135 }
136 pcm.!default {
137 @func getenv
138 vars [ PCM ]
139 default "plug:jack"
140 }
141 '';
142 })
143
144 (mkIf loopback {
145 boot.kernelModules = [ "snd-aloop" ];
146 boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ];
147 sound.extraConfig = cfg.loopback.config;
148 })
149
150 (mkIf cfg.jackd.enable {
151 services.jack.jackd.session = ''
152 ${lib.optionalString bridgeNeeded "${pkgs.a2jmidid}/bin/a2jmidid -e &"}
153 '';
154 # https://alsa.opensrc.org/Jack_and_Loopback_device_as_Alsa-to-Jack_bridge#id06
155 services.jack.loopback.config = ''
156 pcm.loophw00 {
157 type hw
158 card ${toString cfg.loopback.index}
159 device 0
160 subdevice 0
161 }
162 pcm.amix {
163 type dmix
164 ipc_key 219345
165 slave {
166 pcm loophw00
167 ${cfg.loopback.dmixConfig}
168 }
169 }
170 pcm.asoftvol {
171 type softvol
172 slave.pcm "amix"
173 control { name Master }
174 }
175 pcm.cloop {
176 type hw
177 card ${toString cfg.loopback.index}
178 device 1
179 subdevice 0
180 format S32_LE
181 }
182 pcm.loophw01 {
183 type hw
184 card ${toString cfg.loopback.index}
185 device 0
186 subdevice 1
187 }
188 pcm.ploop {
189 type hw
190 card ${toString cfg.loopback.index}
191 device 1
192 subdevice 1
193 format S32_LE
194 }
195 pcm.aduplex {
196 type asym
197 playback.pcm "asoftvol"
198 capture.pcm "loophw01"
199 }
200 pcm.!default {
201 type plug
202 slave.pcm aduplex
203 }
204 '';
205 services.jack.loopback.session = ''
206 alsa_in -j cloop -dcloop &
207 alsa_out -j ploop -dploop &
208 while [ "$(jack_lsp cloop)" == "" ] || [ "$(jack_lsp ploop)" == "" ]; do sleep 1; done
209 jack_connect cloop:capture_1 system:playback_1
210 jack_connect cloop:capture_2 system:playback_2
211 jack_connect system:capture_1 ploop:playback_1
212 jack_connect system:capture_2 ploop:playback_2
213 '';
214
215 assertions = [
216 {
217 assertion = !(cfg.alsa.enable && cfg.loopback.enable);
218 message = "For JACK both alsa and loopback options shouldn't be used at the same time.";
219 }
220 ];
221
222 users.users.jackaudio = {
223 group = "jackaudio";
224 extraGroups = [ "audio" ];
225 description = "JACK Audio system service user";
226 isSystemUser = true;
227 };
228 # http://jackaudio.org/faq/linux_rt_config.html
229 security.pam.loginLimits = [
230 { domain = "@jackaudio"; type = "-"; item = "rtprio"; value = "99"; }
231 { domain = "@jackaudio"; type = "-"; item = "memlock"; value = "unlimited"; }
232 ];
233 users.groups.jackaudio = {};
234
235 environment = {
236 systemPackages = [ cfg.jackd.package ];
237 etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
238 variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
239 };
240
241 services.udev.extraRules = ''
242 ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service"
243 '';
244
245 systemd.services.jack = {
246 description = "JACK Audio Connection Kit";
247 serviceConfig = {
248 User = "jackaudio";
249 SupplementaryGroups = lib.optional
250 (config.hardware.pulseaudio.enable
251 && !config.hardware.pulseaudio.systemWide) "users";
252 ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
253 LimitRTPRIO = 99;
254 LimitMEMLOCK = "infinity";
255 } // optionalAttrs umaskNeeded {
256 UMask = "007";
257 };
258 path = [ cfg.jackd.package ];
259 environment = {
260 JACK_PROMISCUOUS_SERVER = "jackaudio";
261 JACK_NO_AUDIO_RESERVATION = "1";
262 };
263 restartIfChanged = false;
264 };
265 systemd.services.jack-session = {
266 description = "JACK session";
267 script = ''
268 jack_wait -w
269 ${cfg.jackd.session}
270 ${lib.optionalString cfg.loopback.enable cfg.loopback.session}
271 '';
272 serviceConfig = {
273 RemainAfterExit = true;
274 User = "jackaudio";
275 StateDirectory = "jack";
276 LimitRTPRIO = 99;
277 LimitMEMLOCK = "infinity";
278 };
279 path = [ cfg.jackd.package ];
280 environment = {
281 JACK_PROMISCUOUS_SERVER = "jackaudio";
282 HOME = "/var/lib/jack";
283 };
284 wantedBy = [ "jack.service" ];
285 partOf = [ "jack.service" ];
286 after = [ "jack.service" ];
287 restartIfChanged = false;
288 };
289 })
290
291 ];
292
293 meta.maintainers = [ ];
294}