diff options
Diffstat (limited to 'modules/video-audio-recording.el')
| -rw-r--r-- | modules/video-audio-recording.el | 187 |
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. |
