(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-has-live-stream-p (event) "Return non-nil if EVENT has a live stream." (loop for obj across event for info = (gethash "info" obj) when (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)))) do (return t))) (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~%")) (defun process-event (event) "Process one event from pw-dump." (let ((has-live-stream (event-has-live-stream-p event))) (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))