summaryrefslogtreecommitdiff
path: root/modules/video-audio-recording.el
diff options
context:
space:
mode:
Diffstat (limited to 'modules/video-audio-recording.el')
-rw-r--r--modules/video-audio-recording.el187
1 files changed, 176 insertions, 11 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index 73f782f6..45bab267 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -4,7 +4,7 @@
;;; Commentary:
;; Use ffmpeg to record desktop video or just audio.
;; with audio from mic and audio from default audio sink
-;; Also supports audio-only recording in Opus format.
+;; Audio recordings use M4A/AAC format for best compatibility.
;;
;; Note: video-recordings-dir and audio-recordings-dir are defined
;; (and directory created) in user-constants.el
@@ -69,6 +69,160 @@ Returns device name or nil if not found."
(when (string-match "\\([^\t\n]+\\.monitor\\)" output)
(match-string 1 output))))
+(defun cj/recording--parse-pactl-output (output)
+ "Internal parser for pactl sources output. Takes OUTPUT string.
+Returns list of (device-name driver state) tuples.
+Extracted for testing without shell command execution."
+ (let ((sources nil))
+ (dolist (line (split-string output "\n" t))
+ (when (string-match "^[0-9]+\t\\([^\t]+\\)\t\\([^\t]+\\)\t\\([^\t]+\\)\t\\([^\t]+\\)" line)
+ (let ((device (match-string 1 line))
+ (driver (match-string 2 line))
+ (state (match-string 4 line)))
+ (push (list device driver state) sources))))
+ (nreverse sources)))
+
+(defun cj/recording-parse-sources ()
+ "Parse pactl sources output into structured list.
+Returns list of (device-name driver state) tuples."
+ (cj/recording--parse-pactl-output
+ (shell-command-to-string "pactl list sources short 2>/dev/null")))
+
+(defun cj/recording-friendly-state (state)
+ "Convert technical state name to user-friendly label.
+STATE is the raw state from pactl (SUSPENDED, RUNNING, IDLE, etc.)."
+ (pcase state
+ ("SUSPENDED" "Ready")
+ ("RUNNING" "Active")
+ ("IDLE" "Ready")
+ (_ state))) ; fallback to original if unknown
+
+(defun cj/recording-list-devices ()
+ "Show all available audio sources in a readable format.
+Opens a buffer showing devices with their states."
+ (interactive)
+ (let ((sources (cj/recording-parse-sources)))
+ (with-current-buffer (get-buffer-create "*Recording Devices*")
+ (erase-buffer)
+ (insert "Available Audio Sources\n")
+ (insert "========================\n\n")
+ (insert "Note: 'Ready' devices are available and will activate when recording starts.\n\n")
+ (insert "Current Configuration:\n")
+ (insert (format " Microphone: %s\n" (or cj/recording-mic-device "Not set")))
+ (insert (format " System Audio: %s\n\n" (or cj/recording-system-device "Not set")))
+ (insert "Available Devices:\n\n")
+ (if sources
+ (dolist (source sources)
+ (let ((device (nth 0 source))
+ (driver (nth 1 source))
+ (state (nth 2 source))
+ (friendly-state (cj/recording-friendly-state (nth 2 source))))
+ (insert (format "%-10s [%s]\n" friendly-state driver))
+ (insert (format " %s\n\n" device))))
+ (insert " No audio sources found. Is PulseAudio/PipeWire running?\n"))
+ (goto-char (point-min))
+ (special-mode))
+ (switch-to-buffer-other-window "*Recording Devices*")))
+
+(defun cj/recording-select-device (prompt device-type)
+ "Interactively select an audio device.
+PROMPT is shown to user. DEVICE-TYPE is 'mic or 'monitor for filtering.
+Returns selected device name or nil."
+ (let* ((sources (cj/recording-parse-sources))
+ (filtered (if (eq device-type 'monitor)
+ (seq-filter (lambda (s) (string-match-p "\\.monitor$" (car s))) sources)
+ (seq-filter (lambda (s) (not (string-match-p "\\.monitor$" (car s)))) sources)))
+ (choices (mapcar (lambda (s)
+ (let ((device (nth 0 s))
+ (driver (nth 1 s))
+ (state (nth 2 s))
+ (friendly-state (cj/recording-friendly-state (nth 2 s))))
+ (cons (format "%-10s %s" friendly-state device) device)))
+ filtered)))
+ (if choices
+ (cdr (assoc (completing-read prompt choices nil t) choices))
+ (user-error "No %s devices found" (if (eq device-type 'monitor) "monitor" "input")))))
+
+(defun cj/recording-select-devices ()
+ "Interactively select microphone and system audio devices.
+Sets cj/recording-mic-device and cj/recording-system-device."
+ (interactive)
+ (setq cj/recording-mic-device
+ (cj/recording-select-device "Select microphone device: " 'mic))
+ (setq cj/recording-system-device
+ (cj/recording-select-device "Select system audio monitor: " 'monitor))
+ (message "Devices set - Mic: %s, System: %s"
+ cj/recording-mic-device
+ cj/recording-system-device))
+
+(defun cj/recording-group-devices-by-hardware ()
+ "Group audio sources by hardware device.
+Returns alist of (device-name . (mic-source . monitor-source))."
+ (let ((sources (cj/recording-parse-sources))
+ (devices (make-hash-table :test 'equal))
+ (result nil))
+ ;; Group sources by base device name
+ (dolist (source sources)
+ (let* ((device (nth 0 source))
+ (driver (nth 1 source))
+ ;; Extract hardware ID (the unique part that identifies the physical device)
+ (base-name (cond
+ ;; USB devices: extract usb-XXXXX-XX part
+ ((string-match "\\.\\(usb-[^.]+\\-[0-9]+\\)\\." device)
+ (match-string 1 device))
+ ;; Built-in (pci) devices: extract pci-XXXXX part
+ ((string-match "\\.\\(pci-[^.]+\\)\\." device)
+ (match-string 1 device))
+ ;; Bluetooth devices: extract and normalize MAC address
+ ;; (input uses colons, output uses underscores - normalize to colons)
+ ((string-match "bluez_\\(?:input\\|output\\)\\.\\([^.]+\\)" device)
+ (replace-regexp-in-string "_" ":" (match-string 1 device)))
+ (t device)))
+ (is-monitor (string-match-p "\\.monitor$" device))
+ (device-entry (gethash base-name devices)))
+ (unless device-entry
+ (setf device-entry (cons nil nil))
+ (puthash base-name device-entry devices))
+ ;; Store mic or monitor in the pair
+ (if is-monitor
+ (setcdr device-entry device)
+ (setcar device-entry device))))
+
+ ;; Convert hash table to alist with friendly names
+ (maphash (lambda (base-name pair)
+ (when (and (car pair) (cdr pair)) ; Only include if we have both mic and monitor
+ (let ((friendly-name
+ (cond
+ ((string-match-p "usb.*[Jj]abra" base-name) "Jabra SPEAK 510 USB")
+ ((string-match-p "^usb-" base-name) "USB Audio Device")
+ ((string-match-p "^pci-" base-name) "Built-in Laptop Audio")
+ ((string-match-p "^[0-9A-Fa-f:]+$" base-name) "Bluetooth Headset")
+ (t base-name))))
+ (push (cons friendly-name pair) result))))
+ devices)
+ (nreverse result)))
+
+(defun cj/recording-quick-setup-for-calls ()
+ "Quick setup for recording call/meetings.
+Detects available audio devices and lets you pick one device to use for
+both microphone (your voice) and monitor (remote person + sound effects).
+Perfect for recording video calls, phone calls, or presentations."
+ (interactive)
+ (let* ((grouped-devices (cj/recording-group-devices-by-hardware))
+ (choices (mapcar #'car grouped-devices)))
+ (if (null choices)
+ (user-error "No complete audio devices found (need both mic and monitor)")
+ (let* ((choice (completing-read "Which device are you using for the call? " choices nil t))
+ (device-pair (cdr (assoc choice grouped-devices)))
+ (mic (car device-pair))
+ (monitor (cdr device-pair)))
+ (setq cj/recording-mic-device mic)
+ (setq cj/recording-system-device monitor)
+ (message "Call recording ready! Using: %s\n Mic: %s\n Monitor: %s"
+ choice
+ (file-name-nondirectory mic)
+ (file-name-nondirectory monitor))))))
+
(defun cj/recording-get-devices ()
"Get or auto-detect audio devices.
Returns (mic-device . system-device) or nil on error."
@@ -78,9 +232,14 @@ Returns (mic-device . system-device) or nil on error."
(unless cj/recording-system-device
(setq cj/recording-system-device (cj/recording-detect-system-device)))
- ;; Validate devices
+ ;; If auto-detection failed, prompt user to select
(unless (and cj/recording-mic-device cj/recording-system-device)
- (user-error "Could not detect audio devices. Set cj/recording-mic-device and cj/recording-system-device manually"))
+ (when (y-or-n-p "Could not auto-detect audio devices. Select manually? ")
+ (cj/recording-select-devices)))
+
+ ;; Final validation
+ (unless (and cj/recording-mic-device cj/recording-system-device)
+ (user-error "Audio devices not configured. Run M-x cj/recording-select-devices"))
(cons cj/recording-mic-device cj/recording-system-device))
@@ -111,7 +270,7 @@ Otherwise use the default location in `audio-recordings-dir'."
(cj/ffmpeg-record-audio location)))
(defun cj/ffmpeg-record-video (directory)
- "Start an ffmpeg video recording. Save output to DIRECTORY."
+ "Start an ffmpeg video recording. Save output to DIRECTORY."
(cj/recording-check-ffmpeg)
(unless cj/video-recording-ffmpeg-process
(let* ((devices (cj/recording-get-devices))
@@ -144,7 +303,7 @@ Otherwise use the default location in `audio-recordings-dir'."
filename cj/recording-mic-boost cj/recording-system-volume))))
(defun cj/ffmpeg-record-audio (directory)
- "Start an ffmpeg audio recording. Save output to DIRECTORY."
+ "Start an ffmpeg audio recording. Save output to DIRECTORY."
(cj/recording-check-ffmpeg)
(unless cj/audio-recording-ffmpeg-process
(let* ((devices (cj/recording-get-devices))
@@ -152,16 +311,16 @@ Otherwise use the default location in `audio-recordings-dir'."
(system-device (cdr devices))
(location (expand-file-name directory))
(name (format-time-string "%Y-%m-%d-%H-%M-%S"))
- (filename (expand-file-name (concat name ".opus") location))
+ (filename (expand-file-name (concat name ".m4a") location))
(ffmpeg-command
(format (concat "ffmpeg "
"-f pulse -i %s "
"-ac 1 "
"-f pulse -i %s "
- "-ac 2 "
- "-filter_complex \"[0:a]volume=%.1f[mic];[1:a]volume=%.1f[sys];[mic][sys]amerge=inputs=2\" "
- "-c:a libopus "
- "-b:a 96k "
+ "-ac 1 "
+ "-filter_complex \"[0:a]volume=%.1f[mic];[1:a]volume=%.1f[sys];[mic][sys]amerge=inputs=2[out];[out]pan=mono|c0=0.5*c0+0.5*c1\" "
+ "-c:a aac "
+ "-b:a 64k "
"%s")
mic-device
system-device
@@ -222,6 +381,9 @@ Otherwise use the default location in `audio-recordings-dir'."
(define-key map (kbd "A") #'cj/audio-recording-stop)
(define-key map (kbd "a") #'cj/audio-recording-start)
(define-key map (kbd "l") #'cj/recording-adjust-volumes)
+ (define-key map (kbd "d") #'cj/recording-list-devices)
+ (define-key map (kbd "s") #'cj/recording-select-devices)
+ (define-key map (kbd "c") #'cj/recording-quick-setup-for-calls)
map)
"Keymap for video/audio recording operations.")
@@ -234,7 +396,10 @@ Otherwise use the default location in `audio-recordings-dir'."
"C-; r V" "stop video"
"C-; r a" "start audio"
"C-; r A" "stop audio"
- "C-; r l" "adjust levels"))
+ "C-; r l" "adjust levels"
+ "C-; r d" "list devices"
+ "C-; r s" "select devices"
+ "C-; r c" "quick setup for calls"))
(provide 'video-audio-recording)
;;; video-audio-recording.el ends here.