diff options
| -rw-r--r-- | modules/video-audio-recording.el | 57 | ||||
| -rw-r--r-- | tests/test-video-audio-recording--parse-pactl-sinks-verbose.el | 16 | ||||
| -rw-r--r-- | tests/test-video-audio-recording--parse-pactl-sources-verbose.el | 16 |
3 files changed, 28 insertions, 61 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el index 2f0a2298..6664ded1 100644 --- a/modules/video-audio-recording.el +++ b/modules/video-audio-recording.el @@ -267,22 +267,22 @@ for capturing \"what I hear\" regardless of which output hardware is active." (user-error "No default audio output found. Is PulseAudio/PipeWire running?") (concat default-sink ".monitor")))) -(defun cj/recording--parse-pactl-sources-verbose (output) - "Parse verbose `pactl list sources' OUTPUT into structured list. -Returns list of (name description mute state) tuples. -OUTPUT should be the full output of `pactl list sources'." - (let ((sources nil) +(defun cj/recording--parse-pactl-verbose (output record-type) + "Parse verbose pactl OUTPUT into structured list. +RECORD-TYPE is \"Source\" or \"Sink\" — the record header in pactl output. +Returns list of (name description mute state) tuples." + (let ((entries nil) + (header-re (concat "^" record-type " #")) (current-name nil) (current-desc nil) (current-mute nil) (current-state nil)) (dolist (line (split-string output "\n")) (cond - ((string-match "^Source #" line) - ;; Save previous source if complete + ((string-match-p header-re line) (when current-name (push (list current-name current-desc current-mute current-state) - sources)) + entries)) (setq current-name nil current-desc nil current-mute nil current-state nil)) ((string-match "^\\s-+Name:\\s-+\\(.+\\)" line) @@ -293,11 +293,10 @@ OUTPUT should be the full output of `pactl list sources'." (setq current-mute (match-string 1 line))) ((string-match "^\\s-+State:\\s-+\\(.+\\)" line) (setq current-state (match-string 1 line))))) - ;; Don't forget the last source (when current-name (push (list current-name current-desc current-mute current-state) - sources)) - (nreverse sources))) + entries)) + (nreverse entries))) (defun cj/recording--get-available-mics () "Return available microphone sources as list of (name description state mute). @@ -307,7 +306,7 @@ PulseAudio (e.g. \"Jabra SPEAK 510 Mono\") rather than the raw device name. State is the PulseAudio state string (RUNNING, IDLE, or SUSPENDED). Mute is \"yes\" or \"no\"." (let* ((output (shell-command-to-string "pactl list sources 2>/dev/null")) - (sources (cj/recording--parse-pactl-sources-verbose output)) + (sources (cj/recording--parse-pactl-verbose output "Source")) (mics nil)) (dolist (source sources) (let ((name (nth 0 source)) @@ -318,38 +317,6 @@ or SUSPENDED). Mute is \"yes\" or \"no\"." (push (list name (or desc name) state mute) mics)))) (nreverse mics))) -(defun cj/recording--parse-pactl-sinks-verbose (output) - "Parse verbose `pactl list sinks' OUTPUT into structured list. -Returns list of (name description mute state) tuples. -OUTPUT should be the full output of `pactl list sinks'." - (let ((sinks nil) - (current-name nil) - (current-desc nil) - (current-mute nil) - (current-state nil)) - (dolist (line (split-string output "\n")) - (cond - ((string-match "^Sink #" line) - ;; Save previous sink if complete - (when current-name - (push (list current-name current-desc current-mute current-state) - sinks)) - (setq current-name nil current-desc nil - current-mute nil current-state nil)) - ((string-match "^\\s-+Name:\\s-+\\(.+\\)" line) - (setq current-name (match-string 1 line))) - ((string-match "^\\s-+Description:\\s-+\\(.+\\)" line) - (setq current-desc (match-string 1 line))) - ((string-match "^\\s-+Mute:\\s-+\\(.+\\)" line) - (setq current-mute (match-string 1 line))) - ((string-match "^\\s-+State:\\s-+\\(.+\\)" line) - (setq current-state (match-string 1 line))))) - ;; Don't forget the last sink - (when current-name - (push (list current-name current-desc current-mute current-state) - sinks)) - (nreverse sinks))) - (defun cj/recording--get-available-sinks () "Return available audio sinks as list of (name description state mute). Includes muted sinks (shown with a [muted] label in the UI). Uses @@ -357,7 +324,7 @@ the friendly description from PulseAudio (e.g. \"JDS Labs Element IV Analog Stereo\"). State is the PulseAudio state string (RUNNING, IDLE, or SUSPENDED). Mute is \"yes\" or \"no\"." (let* ((output (shell-command-to-string "pactl list sinks 2>/dev/null")) - (sinks (cj/recording--parse-pactl-sinks-verbose output)) + (sinks (cj/recording--parse-pactl-verbose output "Sink")) (result nil)) (dolist (sink sinks) (let ((name (nth 0 sink)) diff --git a/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el b/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el index 8a2cba2d..59159039 100644 --- a/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el +++ b/tests/test-video-audio-recording--parse-pactl-sinks-verbose.el @@ -1,7 +1,7 @@ ;;; test-video-audio-recording--parse-pactl-sinks-verbose.el --- Tests for verbose pactl sinks parser -*- lexical-binding: t; -*- ;;; Commentary: -;; Unit tests for cj/recording--parse-pactl-sinks-verbose. +;; Unit tests for cj/recording--parse-pactl-verbose. ;; Parses the verbose output of `pactl list sinks' into structured tuples ;; of (name description mute state). @@ -34,7 +34,7 @@ (ert-deftest test-parse-pactl-sinks-verbose-normal-multiple-sinks () "Test parsing multiple sink entries from fixture." (let* ((output (test-sinks--fixture "pactl-sinks-verbose-normal.txt")) - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (= 3 (length result))) (should (equal "alsa_output.usb-JDS_Labs-00.analog-stereo" (nth 0 (nth 0 result)))) (should (equal "JDS Labs Element IV Analog Stereo" (nth 1 (nth 0 result)))) @@ -44,7 +44,7 @@ (ert-deftest test-parse-pactl-sinks-verbose-normal-single-sink () "Test parsing a single sink entry." (let* ((output "Sink #65\n\tState: SUSPENDED\n\tName: alsa_output.usb-JDS-00.analog-stereo\n\tDescription: JDS Labs Element IV\n\tMute: no\n") - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (= 1 (length result))) (should (equal "alsa_output.usb-JDS-00.analog-stereo" (nth 0 (car result)))) (should (equal "JDS Labs Element IV" (nth 1 (car result)))) @@ -54,7 +54,7 @@ (ert-deftest test-parse-pactl-sinks-verbose-normal-muted-sink () "Test that muted sinks are parsed (filtering is done by caller)." (let* ((output (test-sinks--fixture "pactl-sinks-verbose-muted.txt")) - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (= 3 (length result))) ;; Second sink is muted (should (equal "yes" (nth 2 (nth 1 result)))))) @@ -63,14 +63,14 @@ (ert-deftest test-parse-pactl-sinks-verbose-boundary-empty-input () "Test that empty input returns empty list." - (should (null (cj/recording--parse-pactl-sinks-verbose "")))) + (should (null (cj/recording--parse-pactl-verbose "" "Sink")))) (ert-deftest test-parse-pactl-sinks-verbose-boundary-extra-fields () "Test that extra fields between sinks are ignored." (let* ((output (concat "Sink #65\n\tState: IDLE\n\tName: sink-a\n\tDescription: Sink A\n\tMute: no\n" "\tDriver: PipeWire\n\tSample Specification: s16le 2ch 48000Hz\n" "Sink #66\n\tState: SUSPENDED\n\tName: sink-b\n\tDescription: Sink B\n\tMute: no\n")) - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (= 2 (length result))) (should (equal "sink-a" (nth 0 (car result)))) (should (equal "sink-b" (nth 0 (cadr result)))))) @@ -78,7 +78,7 @@ (ert-deftest test-parse-pactl-sinks-verbose-boundary-description-with-parens () "Test descriptions containing parentheses are captured fully." (let* ((output "Sink #91\n\tState: SUSPENDED\n\tName: hdmi-output\n\tDescription: Radeon (HDMI 2) Output\n\tMute: no\n") - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (equal "Radeon (HDMI 2) Output" (nth 1 (car result)))))) ;;; Error Cases @@ -86,7 +86,7 @@ (ert-deftest test-parse-pactl-sinks-verbose-error-malformed-no-name () "Test that sink entries without Name field are skipped." (let* ((output "Sink #65\n\tState: SUSPENDED\n\tDescription: Orphan\n\tMute: no\n") - (result (cj/recording--parse-pactl-sinks-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Sink"))) (should (null result)))) (provide 'test-video-audio-recording--parse-pactl-sinks-verbose) diff --git a/tests/test-video-audio-recording--parse-pactl-sources-verbose.el b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el index e856f29a..67945846 100644 --- a/tests/test-video-audio-recording--parse-pactl-sources-verbose.el +++ b/tests/test-video-audio-recording--parse-pactl-sources-verbose.el @@ -1,7 +1,7 @@ ;;; test-video-audio-recording--parse-pactl-sources-verbose.el --- Tests for verbose pactl parser -*- lexical-binding: t; -*- ;;; Commentary: -;; Unit tests for cj/recording--parse-pactl-sources-verbose. +;; Unit tests for cj/recording--parse-pactl-verbose. ;; Parses the verbose output of `pactl list sources' into structured tuples ;; of (name description mute state). @@ -20,7 +20,7 @@ (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-single-source () "Test parsing a single source entry." (let* ((output "Source #65\n\tState: SUSPENDED\n\tName: alsa_input.usb-Jabra-00.mono\n\tDescription: Jabra SPEAK 510 Mono\n\tMute: no\n") - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (= 1 (length result))) (should (equal "alsa_input.usb-Jabra-00.mono" (nth 0 (car result)))) (should (equal "Jabra SPEAK 510 Mono" (nth 1 (car result)))) @@ -31,7 +31,7 @@ "Test parsing multiple source entries." (let* ((output (concat "Source #65\n\tState: SUSPENDED\n\tName: device-a\n\tDescription: Device A\n\tMute: no\n" "Source #66\n\tState: RUNNING\n\tName: device-b\n\tDescription: Device B\n\tMute: yes\n")) - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (= 2 (length result))) (should (equal "device-a" (nth 0 (car result)))) (should (equal "Device B" (nth 1 (cadr result)))) @@ -40,7 +40,7 @@ (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-normal-monitors-included () "Test that monitor sources are parsed (filtering is done by caller)." (let* ((output "Source #67\n\tState: SUSPENDED\n\tName: alsa_output.jds.monitor\n\tDescription: Monitor of JDS Labs\n\tMute: no\n") - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (= 1 (length result))) (should (string-match-p "\\.monitor$" (nth 0 (car result)))))) @@ -48,7 +48,7 @@ (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-empty-input () "Test that empty input returns empty list." - (should (null (cj/recording--parse-pactl-sources-verbose "")))) + (should (null (cj/recording--parse-pactl-verbose "" "Source")))) (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-extra-fields () "Test that extra fields between sources are ignored." @@ -56,7 +56,7 @@ "\tDriver: PipeWire\n\tSample Specification: s16le 2ch 48000Hz\n" "\tChannel Map: front-left,front-right\n" "Source #66\n\tState: SUSPENDED\n\tName: dev-b\n\tDescription: Dev B\n\tMute: no\n")) - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (= 2 (length result))) (should (equal "dev-a" (nth 0 (car result)))) (should (equal "dev-b" (nth 0 (cadr result)))))) @@ -64,7 +64,7 @@ (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-boundary-description-with-parens () "Test descriptions containing parentheses are captured fully." (let* ((output "Source #91\n\tState: SUSPENDED\n\tName: hdmi.monitor\n\tDescription: Monitor of Radeon (HDMI 2)\n\tMute: no\n") - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (equal "Monitor of Radeon (HDMI 2)" (nth 1 (car result)))))) ;;; Error Cases @@ -72,7 +72,7 @@ (ert-deftest test-video-audio-recording--parse-pactl-sources-verbose-error-malformed-no-name () "Test that source entries without Name field are skipped." (let* ((output "Source #65\n\tState: SUSPENDED\n\tDescription: Orphan\n\tMute: no\n") - (result (cj/recording--parse-pactl-sources-verbose output))) + (result (cj/recording--parse-pactl-verbose output "Source"))) (should (null result)))) (provide 'test-video-audio-recording--parse-pactl-sources-verbose) |
