fordon_greeman / voidfiles

Branch: main
Switch branches/tags
main
theidioticdev add emacs 2079ec5 23h ago
359 lines · 12.8 KB Raw Blame History
1 ;;; emms-jack.el --- Jack Audio Connection Kit support -*- lexical-binding: t; -*-
2
3 ;; Copyright (C) 2005-2021 Free Software Foundation, Inc.
4
5 ;; Author: Mario Lang <mlang@delysid.org>
6 ;; Keywords: multimedia, processes
7
8 ;; This file is free software; you can redistribute it and/or modify
9 ;; it under the terms of the GNU General Public License as published by
10 ;; the Free Software Foundation; either version 3, or (at your option)
11 ;; any later version.
12
13 ;; This file is distributed in the hope that it will be useful,
14 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
15 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 ;; GNU General Public License for more details.
17
18 ;; You should have received a copy of the GNU General Public License
19 ;; along with GNU Emacs; see the file COPYING. If not, write to
20 ;; the Free Software Foundation, Inc., 59 Temple Place - Suite 330,
21 ;; Boston, MA 02111-1307, USA.
22
23 ;;; Commentary:
24
25 ;; JACK is a low-latency audio server, written for POSIX conformant
26 ;; operating systems such as GNU/Linux and Apple's OS X. It can connect a
27 ;; number of different applications to an audio device, as well as
28 ;; allowing them to share audio between themselves. Its clients can run in
29 ;; their own processes (ie. as normal applications), or they can run
30 ;; within the JACK server (ie. as a "plugin").
31 ;;
32 ;; JACK was designed from the ground up for professional audio work, and
33 ;; its design focuses on two key areas: synchronous execution of all
34 ;; clients, and low latency operation.
35 ;;
36 ;; jack.el provides a fascility for starting jackd from within Emacs.
37 ;; It also povides convenience functions for prompting the user for
38 ;; jack client and port names in the minibuffer, as well as the
39 ;; functions `jack-connect' and `jack-disconnect' which can be used to
40 ;; rearrange jack port wiring with a minimum of keystrokes.
41
42 ;;; Code:
43
44 (require 'emms-compat)
45
46 (defgroup emms-jack ()
47 "Jack Audio Connection Kit"
48 :group 'processes)
49
50 (defcustom emms-jack-rc '("~/.jackdrc" "/etc/jackd.conf")
51 "JACK run control paths."
52 :type '(repeat string))
53
54 (defcustom emms-jack-use-jack-rc t
55 "If non-nil, try to retrieve jack startup arguments from run control files
56 listed in `jack-rc'. If no rc file is found or this variable is set
57 to nil, use the Emacs variables to build the startup args."
58 :type 'boolean)
59
60 (defcustom emms-jack-program (executable-find "jackd")
61 "JACK executable path."
62 :type 'file)
63
64 (defcustom emms-jack-sample-rate 44100
65 "Default sampling rate for JACK."
66 :type 'integer)
67
68 (defcustom emms-jack-period-size 128
69 "Period size to use when launching new JACK process."
70 :type 'integer)
71
72 (defcustom emms-jack-alsa-device nil
73 "ALSA soundcard to use."
74 :type '(choice (const :tag "Ask" nil) string))
75
76 (defun emms-jack-read-alsa-device ()
77 "Read an ALSA device name using the minibuffer."
78 (let (cards)
79 (with-temp-buffer
80 (insert-file-contents "/proc/asound/cards")
81 (while (not (eobp))
82 (if (looking-at "^\\([0-9]\\) \\[.+\\]: \\(.+\\)\n +\\(.*\\)$")
83 (setq cards (append (list (cons (match-string 3) (match-string 1))) cards)))
84 (forward-line 1)))
85 (concat "hw:" (cdr (assoc (completing-read "Card: " cards nil t) cards)))))
86
87 (defun emms-jack-alsa-device ()
88 (or emms-jack-alsa-device (emms-jack-read-alsa-device)))
89
90 (defcustom emms-jack-output-buffer-name "*JACK output*"
91 "Output buffer name."
92 :type 'string)
93
94 (defun emms-jack-args ()
95 "Return a list of startup arguments to use.
96 First element is the executable path."
97 (or (and emms-jack-use-jack-rc
98 (catch 'rc-found
99 (let ((files (mapcar #'expand-file-name emms-jack-rc)))
100 (while files
101 (if (file-exists-p (car files))
102 (with-temp-buffer
103 (insert-file-contents (car files))
104 (when (> (buffer-size) 0)
105 (throw 'rc-found
106 (split-string (buffer-string) "[\n \t]+")))))
107 (setq files (cdr files))))
108 nil))
109 (list emms-jack-program
110 "-v"
111 "-R"
112 "-dalsa"
113 (format "-d%s" (emms-jack-alsa-device))
114 (format "-r%d" emms-jack-sample-rate)
115 (format "-p%d" emms-jack-period-size))))
116
117 (defcustom emms-jack-set-rtlimits t
118 "Use set_rtlimits (if available) to gain realtime priorities if -R
119 is given in jackd command-line."
120 :type 'boolean)
121
122 (defcustom emms-jack-set-rtlimits-program (executable-find "set_rtlimits")
123 "Path to set_rtlimits."
124 :type 'file)
125
126 (defun emms-jack-maybe-rtlimits (args)
127 (if (and emms-jack-set-rtlimits
128 (or (member "-R" args) (member "--realtime" args))
129 (file-exists-p emms-jack-set-rtlimits-program))
130 (append (list emms-jack-set-rtlimits-program "-r") args)
131 args))
132
133 (defvar emms-jack-process nil)
134
135 (defvar emms-jack-load 0)
136
137 (defvar emms-jack-max-usecs 0)
138
139 (defvar emms-jack-spare 0)
140
141 (defun emms-jack-output-buffer ()
142 (or (get-buffer emms-jack-output-buffer-name)
143 (with-current-buffer (get-buffer-create emms-jack-output-buffer-name)
144 (setq major-mode 'emms-jack-mode
145 mode-name "JACK"
146 mode-line-format (copy-tree mode-line-format))
147 (setcar (nthcdr 16 mode-line-format)
148 `(:eval (format "load:%.2f" emms-jack-load)))
149 (add-hook 'kill-buffer-hook #'emms-jack-kill nil t)
150 (current-buffer))))
151
152 (defvar emms-jack-xruns nil)
153
154 (defun emms-jack-filter (proc string)
155 (with-current-buffer (process-buffer proc)
156 (let ((moving (= (point) (process-mark proc))))
157 (save-excursion
158 (save-match-data
159 (if (string-match "^load = \\([^ ]+\\) max usecs: \\([^,]+\\), spare = \\(.+\\)$" string)
160 (setq emms-jack-load (string-to-number (match-string 1 string))
161 emms-jack-max-usecs (string-to-number (match-string 2 string))
162 emms-jack-spare (string-to-number (match-string 3 string)))
163 (if (string-match "^**** alsa_pcm: xrun of at least \\([^ ]+\\) msecs$" string)
164 (push (string-to-number (match-string 1 string)) emms-jack-xruns)
165 (goto-char (process-mark proc))
166 (insert string)
167 (set-marker (process-mark proc) (point))))))
168 (when moving (goto-char (process-mark proc))))))
169
170 (defun emms-jack-running-p ()
171 (and emms-jack-process (processp emms-jack-process)
172 (eq (process-status emms-jack-process) 'run)))
173
174 (defcustom emms-jack-started-hook nil
175 "Hook run when `emms-jack-start' successfully started a new JACK intance."
176 :type 'hook)
177
178 (defun emms-jack-start ()
179 "Start the JACK process."
180 (interactive)
181 (if (emms-jack-running-p) (error "JACK already running")
182 (setq emms-jack-process
183 (apply #'start-process "jack" (emms-jack-output-buffer)
184 (emms-jack-maybe-rtlimits (emms-jack-args))))
185 (set-process-filter emms-jack-process #'emms-jack-filter)
186 (run-hooks 'emms-jack-started-hook)
187 (switch-to-buffer (emms-jack-output-buffer))))
188
189 (defun emms-jack-kill ()
190 "Kill the currently running JACK process."
191 (interactive)
192 (when (emms-jack-running-p) (delete-process emms-jack-process))
193 (setq emms-jack-process nil))
194
195 (defun emms-jack-restart ()
196 "Restart JACK."
197 (interactive)
198 (if (emms-jack-running-p) (emms-jack-kill))
199 (sit-for 0)
200 (emms-jack-start))
201
202 (defun emms-jack-list ()
203 "Retrieve a list of JACK clients/ports."
204 (with-temp-buffer
205 (call-process "jack_lsp" nil t nil "-cpl")
206 (goto-char (point-min))
207 (let (result current-port)
208 (while (not (eobp))
209 (cond
210 ((looking-at "^\\([^ \t:]+\\):\\(.+\\)$")
211 (let ((program (match-string 1))
212 (port (match-string 2)))
213 (if (assoc program result)
214 (setcdr (assoc program result)
215 (append (cdr (assoc program result)) (list (setq current-port (list port)))))
216 (setq result
217 (append (list (list program (setq current-port (list port)))) result)))))
218 ((looking-at "^ \\([^ \t:]+\\):\\(.+\\)$")
219 (if (assoc 'connections (cdr current-port))
220 (setcdr (assoc 'connections (cdr current-port))
221 (append (cdr (assoc 'connections current-port))
222 (list (list (match-string 1) (match-string 2)))))
223 (setcdr current-port
224 (append (list (list 'connections (list (match-string 1) (match-string 2)))) (cdr current-port)))))
225 ((looking-at "^\tproperties: \\(.+\\),$")
226 (setcdr current-port
227 (append (list (append (list 'properties) (mapcar #'intern (split-string (match-string 1) ",")))) (cdr current-port)))))
228 (forward-line 1))
229 result)))
230
231 (defun emms-jack-ports (program)
232 (cdr (assoc program (emms-jack-list))))
233
234 (defun emms-jack-get-port-connections (program port)
235 (cdr (assoc 'connections (cdr (assoc port (emms-jack-ports program))))))
236
237 (defun emms-jack-get-port-properties (program port)
238 (cdr (assoc 'properties (cdr (assoc port (emms-jack-ports program))))))
239
240 (defun emms-jack-get-direction (program port)
241 (let ((props (emms-jack-get-port-properties program port)))
242 (or (car (member 'output props))
243 (car (member 'input props))
244 (error "Neither input nor output port"))))
245
246 (defun emms-jack-read-program (prompt &optional predicate)
247 (let ((progs (if (functionp predicate)
248 (emms-remove-if-not predicate (emms-jack-list))
249 (emms-jack-list))))
250 (unless progs (error "No matching JACK clients found"))
251 (if (< (length progs) 2) (caar progs)
252 (completing-read prompt progs nil t))))
253
254 (defun emms-jack-unique-port-name (strings)
255 (let ((start "")
256 (maxlen (apply #'min (mapcar #'length strings))))
257 (while (and (< (length start) maxlen)
258 (catch 'not-ok
259 (let ((nextchar (substring (car strings) (length start) (1+ (length start)))))
260 (mapc (lambda (str)
261 (unless (string= (concat start nextchar) (substring str 0 (1+ (length start))))
262 (throw 'not-ok nil)))
263 strings)
264 t)))
265 (setq start (substring (car strings) 0 (1+ (length start)))))
266 start))
267
268 (defun emms-jack-read-port (program prompt &optional predicate)
269 (let ((ports (if (functionp predicate)
270 (emms-remove-if-not predicate (emms-jack-ports program))
271 (emms-jack-ports program))))
272 (if (< (length ports) 2) (caar ports)
273 (completing-read prompt ports nil t
274 (emms-jack-unique-port-name (mapcar #'car ports))))))
275
276 (defun emms-jack-connect (from-program from-port to-program to-port)
277 "Connect FROM-PROGRAM's output port FROM-PORT to TO-PROGRAM's input port
278 TO-PORT.
279 If called interactively, the direction does not matter."
280 (interactive
281 (let* ((prog (emms-jack-read-program "Connect: "))
282 (port (emms-jack-read-port prog (format "Connect %s port: " prog)))
283 (to-type (if (eq (emms-jack-get-direction prog port) 'input) 'output 'input))
284 (to-prog (emms-jack-read-program
285 (format "Connect %s port %s to: " prog port)
286 (lambda (prog)
287 (emms-find-if (lambda (port)
288 (member to-type (assoc 'properties
289 (cdr port))))
290 (cdr prog)))))
291 (to-port (emms-jack-read-port
292 to-prog
293 (format "Connect %s port %s to %s port: " prog port to-prog)
294 (lambda (port)
295 (member to-type (cdr (assoc 'properties (cdr port))))))))
296 (if (eq to-type 'input)
297 (list prog port to-prog to-port)
298 (list to-prog to-port prog port))))
299 (let ((result (call-process "jack_connect" nil nil nil
300 (format "%s:%s" from-program from-port)
301 (format "%s:%s" to-program to-port))))
302 (if (= result 0)
303 (message "JACK: Connected %s:%s to %s:%s"
304 from-program from-port to-program to-port))))
305
306 (defun emms-jack-disconnect (from-program from-port to-program to-port)
307 "Disconnect FROM-PROGRAM's output port FROM-PORT from TO-PROGRAM's
308 input port TO-PORT.
309 If called interactively, the direction is not relevant."
310 (interactive
311 (let* ((prog (emms-jack-read-program
312 "Disconnect: "
313 (lambda (prog)
314 (emms-find-if (lambda (port) (assoc 'connections (cdr port)))
315 (cdr prog)))))
316 (port (emms-jack-read-port prog
317 (format "Disconnect %s port: " prog)
318 (lambda (port)
319 (assoc 'connections (cdr port)))))
320 (connections (emms-jack-get-port-connections prog port))
321 (from (list prog port))
322 (to (if (< (length connections) 2)
323 (car connections)
324 (let* ((to-progs (let (result)
325 (mapc (lambda (conn)
326 (if (not (member (car conn) result))
327 (setq result
328 (append (list (car conn))
329 result))))
330 connections)
331 (mapcar #'list result)))
332 (to-prog (if (< (length to-progs) 2)
333 (caar to-progs)
334 (completing-read
335 (format "Disconnect %s port %s from: "
336 prog port) to-progs nil t))))
337 (setq connections (emms-remove-if-not
338 (lambda (conn)
339 (string= (car conn) to-prog))
340 connections))
341 (if (< (length connections) 2)
342 (car connections)
343 (let ((to-port (completing-read
344 (format "Disconnect %s port %s from %s port: "
345 prog port to-prog)
346 (mapcar #'cdr connections) nil t)))
347 (list to-prog to-port)))))))
348 (if (eq (emms-jack-get-direction prog port) 'output)
349 (append from to)
350 (append to from))))
351 (let ((result (call-process "jack_disconnect" nil nil nil
352 (format "%s:%s" from-program from-port)
353 (format "%s:%s" to-program to-port))))
354 (if (= result 0)
355 (message "JACK: Disconnected %s:%s from %s:%s"
356 from-program from-port to-program to-port))))
357
358 (provide 'emms-jack)
359 ;;; emms-jack.el ends here