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.el128
1 files changed, 108 insertions, 20 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index f9311957..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,11 +69,11 @@ Returns device name or nil if not found."
(when (string-match "\\([^\t\n]+\\.monitor\\)" output)
(match-string 1 output))))
-(defun cj/recording-parse-sources ()
- "Parse pactl sources output into structured list.
-Returns list of (device-name description state) tuples."
- (let ((output (shell-command-to-string "pactl list sources short 2>/dev/null"))
- (sources nil))
+(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))
@@ -82,6 +82,21 @@ Returns list of (device-name description state) tuples."
(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."
@@ -91,6 +106,7 @@ Opens a buffer showing devices with their states."
(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")))
@@ -99,8 +115,9 @@ Opens a buffer showing devices with their states."
(dolist (source sources)
(let ((device (nth 0 source))
(driver (nth 1 source))
- (state (nth 2 source)))
- (insert (format "%-10s [%s]\n" state driver))
+ (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))
@@ -118,8 +135,9 @@ Returns selected device name or nil."
(choices (mapcar (lambda (s)
(let ((device (nth 0 s))
(driver (nth 1 s))
- (state (nth 2 s)))
- (cons (format "%-10s %s" state device) device)))
+ (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))
@@ -137,6 +155,74 @@ Sets cj/recording-mic-device and cj/recording-system-device."
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."
@@ -148,12 +234,12 @@ Returns (mic-device . system-device) or nil on error."
;; If auto-detection failed, prompt user to select
(unless (and cj/recording-mic-device cj/recording-system-device)
- (when (y-or-n-p "Could not auto-detect audio devices. Select 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"))
+ (user-error "Audio devices not configured. Run M-x cj/recording-select-devices"))
(cons cj/recording-mic-device cj/recording-system-device))
@@ -184,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))
@@ -217,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))
@@ -225,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
@@ -297,6 +383,7 @@ Otherwise use the default location in `audio-recordings-dir'."
(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.")
@@ -311,7 +398,8 @@ Otherwise use the default location in `audio-recordings-dir'."
"C-; r A" "stop audio"
"C-; r l" "adjust levels"
"C-; r d" "list devices"
- "C-; r s" "select devices"))
+ "C-; r s" "select devices"
+ "C-; r c" "quick setup for calls"))
(provide 'video-audio-recording)
;;; video-audio-recording.el ends here.