| 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 |