summaryrefslogtreecommitdiff
path: root/modules
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-02-26 17:08:19 -0600
committerCraig Jennings <c@cjennings.net>2026-02-26 17:08:19 -0600
commit58ef63abd5a9187ee93609f142cb21a933da16c5 (patch)
tree048052e931d218cac1930d736739e11e491b4e7b /modules
parentf473f610b7fccffd3d10d8e81342218cd4ab25fc (diff)
feat(recording): validate system audio device before recording
Add pre-recording validation that catches stale or drifted system audio devices before they cause silent recordings. When the default audio output changes (Bluetooth reconnect, device switch) between setup and recording, the monitor device is auto-updated. Warns if no audio is currently playing through the monitored sink. Co-Authored-By: Craig Jennings <c@cjennings.net>
Diffstat (limited to 'modules')
-rw-r--r--modules/video-audio-recording.el81
1 files changed, 81 insertions, 0 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 5e812881..6ab617ec 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -574,8 +574,89 @@ If devices aren't set, goes straight into quick setup (mic selection)."
(cj/recording-quick-setup))
(unless (and cj/recording-mic-device cj/recording-system-device)
(user-error "Audio devices not configured. Run C-; r s (quick setup) or C-; r S (manual select)"))
+ (cj/recording--validate-system-audio)
(cons cj/recording-mic-device cj/recording-system-device))
+(defun cj/recording--source-exists-p (source-name pactl-output)
+ "Return non-nil if SOURCE-NAME exists in PACTL-OUTPUT.
+PACTL-OUTPUT should be the output of `pactl list sources short'."
+ (let ((found nil))
+ (dolist (line (split-string pactl-output "\n" t))
+ (when (string-match "^[0-9]+\t\\([^\t]+\\)\t" line)
+ (when (equal source-name (match-string 1 line))
+ (setq found t))))
+ found))
+
+(defun cj/recording--get-sink-index (sink-name sinks-output)
+ "Return the numeric index of SINK-NAME from SINKS-OUTPUT.
+SINKS-OUTPUT should be the output of `pactl list sinks short'.
+Returns the index as a string, or nil if not found."
+ (let ((index nil))
+ (dolist (line (split-string sinks-output "\n" t))
+ (when (string-match "^\\([0-9]+\\)\t\\([^\t]+\\)\t" line)
+ (when (equal sink-name (match-string 2 line))
+ (setq index (match-string 1 line)))))
+ index))
+
+(defun cj/recording--sink-has-active-audio-p (sink-index pactl-output)
+ "Return non-nil if SINK-INDEX has active audio streams.
+PACTL-OUTPUT should be the output of `pactl list sink-inputs'.
+SINK-INDEX is the numeric sink index as a string."
+ (let ((found nil)
+ (lines (split-string pactl-output "\n")))
+ (dolist (line lines)
+ (when (string-match "^[ \t]+Sink:[ \t]+\\([0-9]+\\)" line)
+ (when (equal sink-index (match-string 1 line))
+ (setq found t))))
+ found))
+
+(defun cj/recording--validate-system-audio ()
+ "Validate that the configured system audio device will capture audio.
+Checks three things:
+1. Does the configured device still exist as a PulseAudio source?
+2. Has the default sink drifted from what we're monitoring?
+3. Is anything currently playing through the monitored sink?
+
+Auto-fixes stale/drifted devices. Warns (but doesn't block) if no audio
+is currently playing."
+ (when cj/recording-system-device
+ (let* ((sources-output (shell-command-to-string "pactl list sources short 2>/dev/null"))
+ (current-default (cj/recording--get-default-sink-monitor))
+ (device-exists (cj/recording--source-exists-p
+ cj/recording-system-device sources-output)))
+ ;; Check 1: Device no longer exists — auto-update
+ (unless device-exists
+ (let ((old cj/recording-system-device))
+ (setq cj/recording-system-device current-default)
+ (message "System audio device updated: %s → %s (old device no longer exists)"
+ old current-default)))
+ ;; Check 2: Default sink has drifted — auto-update
+ (when (and device-exists
+ (not (equal cj/recording-system-device current-default)))
+ (let ((old cj/recording-system-device))
+ (setq cj/recording-system-device current-default)
+ (message "System audio device updated: %s → %s (default output changed)"
+ old current-default)))
+ ;; Check 3: No active audio on the monitored sink — warn
+ (let* ((sink-name (if (string-suffix-p ".monitor" cj/recording-system-device)
+ (substring cj/recording-system-device 0 -8)
+ cj/recording-system-device))
+ (sinks-output (shell-command-to-string "pactl list sinks short 2>/dev/null"))
+ (sink-index (cj/recording--get-sink-index sink-name sinks-output))
+ (sink-inputs (shell-command-to-string "pactl list sink-inputs 2>/dev/null"))
+ (has-audio (and sink-index
+ (cj/recording--sink-has-active-audio-p sink-index sink-inputs))))
+ (unless has-audio
+ (unless (y-or-n-p
+ (format (concat "Warning: No audio is playing through %s.\n"
+ "If you're in a meeting, the other participants may not be recorded.\n"
+ "- Check that your call app is using the expected audio output\n"
+ "- Run C-; r w to see which device your call is using\n"
+ "- Run C-; r s to switch devices\n"
+ "Continue anyway? ")
+ sink-name))
+ (user-error "Recording cancelled")))))))
+
;;; ============================================================
;;; Toggle Commands (User-Facing)
;;; ============================================================