1{
2 config,
3 lib,
4 pkgs,
5 ...
6}:
7let
8 cfg = config.services.jack;
9
10 pcmPlugin = cfg.jackd.enable && cfg.alsa.enable;
11 loopback = cfg.jackd.enable && cfg.loopback.enable;
12
13 enable32BitAlsaPlugins =
14 cfg.alsa.support32Bit && pkgs.stdenv.hostPlatform.isx86_64 && pkgs.pkgsi686Linux.alsa-lib != null;
15
16 umaskNeeded = lib.versionOlder cfg.jackd.package.version "1.9.12";
17 bridgeNeeded = lib.versionAtLeast cfg.jackd.package.version "1.9.12";
18in
19{
20 options = {
21 services.jack = {
22 jackd = {
23 enable = lib.mkEnableOption ''
24 JACK Audio Connection Kit. You need to add yourself to the "jackaudio" group
25 '';
26
27 package =
28 lib.mkPackageOption pkgs "jack2" {
29 example = "jack1";
30 }
31 // {
32 # until jack1 promiscuous mode is fixed
33 internal = true;
34 };
35
36 extraOptions = lib.mkOption {
37 type = lib.types.listOf lib.types.str;
38 default = [
39 "-dalsa"
40 ];
41 example = lib.literalExpression ''
42 [ "-dalsa" "--device" "hw:1" ];
43 '';
44 description = ''
45 Specifies startup command line arguments to pass to JACK server.
46 '';
47 };
48
49 session = lib.mkOption {
50 type = lib.types.lines;
51 description = ''
52 Commands to run after JACK is started.
53 '';
54 };
55
56 };
57
58 alsa = {
59 enable = lib.mkOption {
60 type = lib.types.bool;
61 default = true;
62 description = ''
63 Route audio to/from generic ALSA-using applications using ALSA JACK PCM plugin.
64 '';
65 };
66
67 support32Bit = lib.mkOption {
68 type = lib.types.bool;
69 default = false;
70 description = ''
71 Whether to support sound for 32-bit ALSA applications on 64-bit system.
72 '';
73 };
74 };
75
76 loopback = {
77 enable = lib.mkOption {
78 type = lib.types.bool;
79 default = false;
80 description = ''
81 Create ALSA loopback device, instead of using PCM plugin. Has broader
82 application support (things like Steam will work), but may need fine-tuning
83 for concrete hardware.
84 '';
85 };
86
87 index = lib.mkOption {
88 type = lib.types.int;
89 default = 10;
90 description = ''
91 Index of an ALSA loopback device.
92 '';
93 };
94
95 config = lib.mkOption {
96 type = lib.types.lines;
97 description = ''
98 ALSA config for loopback device.
99 '';
100 };
101
102 dmixConfig = lib.mkOption {
103 type = lib.types.lines;
104 default = "";
105 example = ''
106 period_size 2048
107 periods 2
108 '';
109 description = ''
110 For music production software that still doesn't support JACK natively you
111 would like to put buffer/period adjustments here
112 to decrease dmix device latency.
113 '';
114 };
115
116 session = lib.mkOption {
117 type = lib.types.lines;
118 description = ''
119 Additional commands to run to setup loopback device.
120 '';
121 };
122 };
123
124 };
125
126 };
127
128 config = lib.mkMerge [
129
130 (lib.mkIf pcmPlugin {
131 environment.etc."alsa/conf.d/98-jack.conf".text = ''
132 pcm_type.jack {
133 libs.native = ${pkgs.alsa-plugins}/lib/alsa-lib/libasound_module_pcm_jack.so ;
134 ${lib.optionalString enable32BitAlsaPlugins "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 (lib.mkIf loopback {
145 boot.kernelModules = [ "snd-aloop" ];
146 boot.kernelParams = [ "snd-aloop.index=${toString cfg.loopback.index}" ];
147 environment.etc."alsa/conf.d/99-jack-loopback.conf".text = cfg.loopback.config;
148 })
149
150 (lib.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 # https://jackaudio.org/faq/linux_rt_config.html
229 security.pam.loginLimits = [
230 {
231 domain = "@jackaudio";
232 type = "-";
233 item = "rtprio";
234 value = "99";
235 }
236 {
237 domain = "@jackaudio";
238 type = "-";
239 item = "memlock";
240 value = "unlimited";
241 }
242 ];
243 users.groups.jackaudio = { };
244
245 environment = {
246 systemPackages = [ cfg.jackd.package ];
247 etc."alsa/conf.d/50-jack.conf".source = "${pkgs.alsa-plugins}/etc/alsa/conf.d/50-jack.conf";
248 variables.JACK_PROMISCUOUS_SERVER = "jackaudio";
249 };
250
251 services.udev.extraRules = ''
252 ACTION=="add", SUBSYSTEM=="sound", ATTRS{id}!="Loopback", TAG+="systemd", ENV{SYSTEMD_WANTS}="jack.service"
253 '';
254
255 systemd.services.jack = {
256 description = "JACK Audio Connection Kit";
257 serviceConfig =
258 {
259 User = "jackaudio";
260 SupplementaryGroups = lib.optional (
261 config.services.pulseaudio.enable && !config.services.pulseaudio.systemWide
262 ) "users";
263 ExecStart = "${cfg.jackd.package}/bin/jackd ${lib.escapeShellArgs cfg.jackd.extraOptions}";
264 LimitRTPRIO = 99;
265 LimitMEMLOCK = "infinity";
266 }
267 // lib.optionalAttrs umaskNeeded {
268 UMask = "007";
269 };
270 path = [ cfg.jackd.package ];
271 environment = {
272 JACK_PROMISCUOUS_SERVER = "jackaudio";
273 JACK_NO_AUDIO_RESERVATION = "1";
274 };
275 restartIfChanged = false;
276 };
277 systemd.services.jack-session = {
278 description = "JACK session";
279 script = ''
280 ${pkgs.jack-example-tools}/bin/jack_wait -w
281 ${cfg.jackd.session}
282 ${lib.optionalString cfg.loopback.enable cfg.loopback.session}
283 '';
284 serviceConfig = {
285 RemainAfterExit = true;
286 User = "jackaudio";
287 StateDirectory = "jack";
288 LimitRTPRIO = 99;
289 LimitMEMLOCK = "infinity";
290 };
291 path = [ cfg.jackd.package ];
292 environment = {
293 JACK_PROMISCUOUS_SERVER = "jackaudio";
294 HOME = "/var/lib/jack";
295 };
296 wantedBy = [ "jack.service" ];
297 partOf = [ "jack.service" ];
298 after = [ "jack.service" ];
299 restartIfChanged = false;
300 };
301 })
302
303 ];
304
305 meta.maintainers = [ ];
306}