diff options
| author | Craig Jennings <c@cjennings.net> | 2026-02-26 17:08:19 -0600 |
|---|---|---|
| committer | Craig Jennings <c@cjennings.net> | 2026-02-26 17:08:19 -0600 |
| commit | 58ef63abd5a9187ee93609f142cb21a933da16c5 (patch) | |
| tree | 048052e931d218cac1930d736739e11e491b4e7b /modules | |
| parent | f473f610b7fccffd3d10d8e81342218cd4ab25fc (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.el | 81 |
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) ;;; ============================================================ |
