From 99f991728b98479d76f2d756254038aa303ffdfb Mon Sep 17 00:00:00 2001 From: Alexander Rosenberg Date: Fri, 10 Oct 2025 02:32:08 -0700 Subject: [PATCH] Add inhibit-sleep-for-audio.lisp --- cl/inhibit-sleep-for-audio.lisp | 113 ++++++++++++++++++++++++++++++++ 1 file changed, 113 insertions(+) create mode 100644 cl/inhibit-sleep-for-audio.lisp diff --git a/cl/inhibit-sleep-for-audio.lisp b/cl/inhibit-sleep-for-audio.lisp new file mode 100644 index 0000000..55b9a3e --- /dev/null +++ b/cl/inhibit-sleep-for-audio.lisp @@ -0,0 +1,113 @@ +(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))