(eval-when (:compile-toplevel :load-toplevel :execute) (ql:quickload '(:uiop :com.inuoe.jzon))) (defpackage :inhibit-sleep-for-audio (:use :cl) (:local-nicknames (:jzon :com.inuoe.jzon)) (:export :toplevel)) (in-package :inhibit-sleep-for-audio) (defparameter *debug-output* (progn #+slynk t #-slynk nil) ;; unconfuse emacs "Whether or not to print debug output.") (declaim (inline debug-format)) (defun debug-format (control-string &rest args) "FORMAT to stdout, but only when *debug-output* is non-nil." (when *debug-output* (apply 'format t control-string args))) (defun event-stream-states (event) "Return a alist mapping ids to stream states." (loop for obj across event for info = (gethash "info" obj) for id = (gethash "id" obj) for running = (and (hash-table-p info) (equal (gethash "type" obj) "PipeWire:Interface:Node") (equal (gethash "state" info) "running") (or (not (equal (gethash "n-input-ports" info) 0)) (not (equal (gethash "n-output-ports" info) 0)))) collect (cons id running))) (defvar *inhibitor-process* nil "The systemd-inhibit process object.") (defun inhibitor-running-p () "Return non-nil if the inhibitor is active." (and *inhibitor-process* (uiop:process-alive-p *inhibitor-process*))) (defun start-inhibitor () "Start the inhibitor process." (let ((cmd (list "systemd-inhibit" "--mode=block" "--what=sleep:idle" "--who=inhibit-sleep-for-audio" "--why=PipeWire audio playing or recording" "sleep" (princ-to-string most-positive-fixnum)))) (setq *inhibitor-process* (uiop:launch-program cmd :output "/dev/null")) (debug-format "Started inhibitor process ~S~%" cmd))) (defun stop-inhibitor () "Stop the inhibitor process." (uiop:terminate-process *inhibitor-process*) (uiop:wait-process *inhibitor-process*) (setq *inhibitor-process* nil) (debug-format "Stopped inhibitor process~%")) (defvar *running-streams* (make-hash-table :test #'eql) "Hash table (set) of running streams. This maps ids to the symbol t.") (defun have-running-streams-p () "Return non-nil if there are running streams." (not (zerop (hash-table-count *running-streams*)))) (defun process-event (event) "Process one event from pw-dump." (let ((live-streams (event-stream-states event))) (dolist (entry live-streams) (destructuring-bind (id . state) entry (if state (progn (when (not (gethash id *running-streams*)) (debug-format "Stream ~A started~%" id)) (setf (gethash id *running-streams*) t)) (progn (when (gethash id *running-streams*) (debug-format "Stream ~A stopped~%" id)) (remhash id *running-streams*))))) (let ((has-live-stream (have-running-streams-p))) (cond ((and has-live-stream (not (inhibitor-running-p))) (start-inhibitor)) ((and (not has-live-stream) (inhibitor-running-p)) (stop-inhibitor)))))) (defun print-help-and-exit () "Print a help message and then exit." (format t "usage: ~A [-h|--help] [-d|--debug] -h|--help print this message, then exit -d|--debug print debug output as program runs~%" (or (uiop:argv0) "inhibit-sleep-for-audio.lisp")) #-slynk (uiop:quit)) (defun handle-cli-args () "Process command-line arguments." (dolist (arg (uiop:command-line-arguments)) (when (or (equal arg "-h") (equal arg "--help")) (print-help-and-exit)) (when (or (equal arg "-d") (equal arg "--debug")) (setq *debug-output* t)))) (defun read-next-event (stream) "Read the next pw-dump event from STREAM." (jzon:with-parser (parse stream) (jzon:parse-next-element parse))) (defun main () (handle-cli-args) (let ((monitor-process (uiop:launch-program '("pw-dump" "-m") :output :stream))) (unwind-protect (progn (debug-format "Started pw-dump monitor process with pid ~A~%" (uiop:process-info-pid monitor-process)) (loop with stream = (uiop:process-info-output monitor-process) while (uiop:process-alive-p monitor-process) do (process-event (read-next-event stream)))) (when (inhibitor-running-p) (stop-inhibitor)) (when (uiop:process-alive-p monitor-process) (uiop:terminate-process monitor-process) (uiop:wait-process monitor-process) (debug-format "Terminated pw-dump monitor process...~%"))))) #+sbcl (sb-ext:disable-debugger) (defun toplevel () "Toplevel of the program." #+sbcl (handler-case (main) (sb-sys:interactive-interrupt () (format t "Exiting because of keyboard interrupt...~%") (uiop:quit 1))) #-sbcl (main))