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.el501
1 files changed, 442 insertions, 59 deletions
diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el
index fa4c2926..b3151dba 100644
--- a/modules/video-audio-recording.el
+++ b/modules/video-audio-recording.el
@@ -3,12 +3,56 @@
;;
;;; 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.
+;; Records audio from both microphone and system audio (for calls/meetings).
+;; 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
;;
+;; Quick Start
+;; ===========
+;; 1. Press C-; r a (start/stop audio recording)
+;; 2. First time: you'll be prompted for device setup
+;; 3. Choose "Bluetooth Headset" (or your device)
+;; 4. Recording starts - you'll see 🔴Audio in your modeline
+;; 5. Press C-; r a again to stop (🔴 disappears)
+;;
+;; Device Setup (First Time Only)
+;; ===============================
+;; C-; r a automatically prompts for device selection on first use.
+;; Device selection persists across Emacs sessions.
+;;
+;; Manual device selection:
+;;
+;; C-; r c (cj/recording-quick-setup-for-calls) - RECOMMENDED
+;; Quick setup: picks one device for both mic and monitor.
+;; Perfect for calls, meetings, or when using headset.
+;;
+;; C-; r s (cj/recording-select-devices) - ADVANCED
+;; Manual selection: choose mic and monitor separately.
+;; Use when you need different devices for input/output.
+;;
+;; C-; r d (cj/recording-list-devices)
+;; List all available audio devices and current configuration.
+;;
+;; C-; r w (cj/recording-show-active-audio) - DIAGNOSTIC TOOL
+;; Show which apps are currently playing audio and through which device.
+;; Use this DURING a phone call to see if the call audio is going through
+;; the device you think it is. Helps diagnose "missing one side" issues.
+;;
+;; Testing Devices Before Important Recordings
+;; ============================================
+;; Always test devices before important meetings/calls:
+;;
+;; C-; r t b (cj/recording-test-both) - RECOMMENDED
+;; Guided test: mic only, monitor only, then both together.
+;; Catches hardware issues before they ruin recordings!
+;;
+;; C-; r t m (cj/recording-test-mic)
+;; Quick 5-second mic test with playback.
+;;
+;; C-; r t s (cj/recording-test-monitor)
+;; Quick 5-second system audio test with playback.
;;
;; To adjust volumes:
;; - Use =M-x cj/recording-adjust-volumes= (or your keybinding =r l=)
@@ -20,6 +64,8 @@
;;
;;; Code:
+(require 'system-lib)
+
;; Forward declarations
(eval-when-compile (defvar video-recordings-dir))
(eval-when-compile (defvar audio-recordings-dir))
@@ -29,9 +75,10 @@
"Volume multiplier for microphone in recordings.
1.0 = normal volume, 2.0 = double volume (+6dB), 0.5 = half volume (-6dB).")
-(defvar cj/recording-system-volume 0.5
+(defvar cj/recording-system-volume 2.0
"Volume multiplier for system audio in recordings.
-1.0 = normal volume, 2.0 = double volume (+6dB), 0.5 = half volume (-6dB).")
+1.0 = normal volume, 2.0 = double volume (+6dB), 0.5 = half volume (-6dB).
+Default is 2.0 because the pan filter reduces by 50%, so final level is 1.0x.")
(defvar cj/recording-mic-device nil
"PulseAudio device name for microphone input.
@@ -47,6 +94,36 @@ If nil, will auto-detect on first use.")
(defvar cj/audio-recording-ffmpeg-process nil
"Variable to store the process of the ffmpeg audio recording.")
+;; Modeline recording indicator
+(defun cj/recording-modeline-indicator ()
+ "Return modeline string showing active recordings.
+Shows 🔴 when recording (audio and/or video).
+Checks if process is actually alive, not just if variable is set."
+ (let ((audio-active (and cj/audio-recording-ffmpeg-process
+ (process-live-p cj/audio-recording-ffmpeg-process)))
+ (video-active (and cj/video-recording-ffmpeg-process
+ (process-live-p cj/video-recording-ffmpeg-process))))
+ (cond
+ ((and audio-active video-active) " 🔴A+V ")
+ (audio-active " 🔴Audio ")
+ (video-active " 🔴Video ")
+ (t ""))))
+
+(defun cj/recording-process-sentinel (process event)
+ "Sentinel for recording processes to clean up and update modeline.
+PROCESS is the ffmpeg process, EVENT describes what happened."
+ (when (memq (process-status process) '(exit signal))
+ ;; Process ended - clear the variable
+ (cond
+ ((eq process cj/audio-recording-ffmpeg-process)
+ (setq cj/audio-recording-ffmpeg-process nil)
+ (message "Audio recording stopped: %s" (string-trim event)))
+ ((eq process cj/video-recording-ffmpeg-process)
+ (setq cj/video-recording-ffmpeg-process nil)
+ (message "Video recording stopped: %s" (string-trim event))))
+ ;; Force modeline update
+ (force-mode-line-update t)))
+
(defun cj/recording-check-ffmpeg ()
"Check if ffmpeg is available.
Return t if found, nil otherwise."
@@ -55,63 +132,332 @@ Return t if found, nil otherwise."
nil)
t)
-(defun cj/recording-detect-mic-device ()
- "Auto-detect PulseAudio microphone input device.
-Returns device name or nil if not found."
- (let ((output (shell-command-to-string "pactl list sources short 2>/dev/null")))
- (when (string-match "\\([^\t\n]+\\).*analog.*stereo" output)
- (match-string 1 output))))
+;; Auto-detection functions removed - they were unreliable and preferred built-in
+;; audio over Bluetooth/USB devices. Use explicit device selection instead:
+;; - C-; r c (cj/recording-quick-setup-for-calls) - recommended for most use cases
+;; - C-; r s (cj/recording-select-devices) - manual selection of mic + monitor
-(defun cj/recording-detect-system-device ()
- "Auto-detect PulseAudio system audio monitor device.
-Returns device name or nil if not found."
- (let ((output (shell-command-to-string "pactl list sources short 2>/dev/null")))
- (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-get-devices ()
- "Get or auto-detect audio devices.
-Returns (mic-device . system-device) or nil on error."
- ;; Auto-detect if not already set
+(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-show-active-audio ()
+ "Show which audio sinks are currently PLAYING audio in real-time.
+Useful for diagnosing why phone call audio isn't being captured - helps identify
+which device the phone app is actually using for output."
+ (interactive)
+ (let ((output (shell-command-to-string "pactl list sink-inputs")))
+ (with-current-buffer (get-buffer-create "*Active Audio Playback*")
+ (erase-buffer)
+ (insert "Active Audio Playback (Updated: " (format-time-string "%H:%M:%S") ")\n")
+ (insert "======================================================\n\n")
+ (insert "This shows which applications are CURRENTLY playing audio and through which device.\n")
+ (insert "If you're on a phone call, you should see the phone app listed here.\n")
+ (insert "The 'Sink' line shows which output device it's using.\n\n")
+ (if (string-match-p "Sink Input" output)
+ (progn
+ (insert output)
+ (insert "\n\nTIP: The '.monitor' device corresponding to the 'Sink' above is what\n")
+ (insert "you need to select for system audio to capture the other person's voice.\n\n")
+ (insert "For example, if Sink is 'alsa_output.usb...Jabra...analog-stereo',\n")
+ (insert "then you need 'alsa_output.usb...Jabra...analog-stereo.monitor'\n"))
+ (insert "No active audio playback detected.\n\n")
+ (insert "This means no applications are currently playing audio.\n")
+ (insert "If you're on a phone call and see this, the phone app might be:\n")
+ (insert " 1. Using a different audio system (not PulseAudio/PipeWire)\n")
+ (insert " 2. Using a Bluetooth device directly (bypassing system audio)\n")
+ (insert " 3. Not actually playing audio (check if you can hear the other person)\n"))
+ (goto-char (point-min))
+ (special-mode))
+ (switch-to-buffer-other-window "*Active Audio Playback*")
+ (message "Showing active audio playback. Press 'g' to refresh, 'q' to quit.")))
+
+(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-test-mic ()
+ "Test microphone by recording 5 seconds and playing it back.
+Records from configured mic device, saves to temp file, plays back.
+Useful for verifying mic hardware works before important recordings."
+ (interactive)
(unless cj/recording-mic-device
- (setq cj/recording-mic-device (cj/recording-detect-mic-device)))
+ (user-error "No microphone configured. Run C-; r c first"))
+
+ (let* ((temp-file (make-temp-file "mic-test-" nil ".wav"))
+ (duration 5))
+ (message "Recording from mic for %d seconds... SPEAK NOW!" duration)
+ (shell-command
+ (format "ffmpeg -f pulse -i %s -t %d -y %s 2>/dev/null"
+ (shell-quote-argument cj/recording-mic-device)
+ duration
+ (shell-quote-argument temp-file)))
+ (message "Playing back recording...")
+ (shell-command (format "ffplay -autoexit -nodisp %s 2>/dev/null &"
+ (shell-quote-argument temp-file)))
+ (message "Mic test complete. Temp file: %s" temp-file)))
+
+(defun cj/recording-test-monitor ()
+ "Test system audio monitor by recording 5 seconds and playing it back.
+Records from configured monitor device (system audio output).
+Play some audio/video during test. Useful for verifying you can capture
+conference call audio, YouTube, etc."
+ (interactive)
(unless cj/recording-system-device
- (setq cj/recording-system-device (cj/recording-detect-system-device)))
+ (user-error "No system monitor configured. Run C-; r c first"))
+
+ (let* ((temp-file (make-temp-file "monitor-test-" nil ".wav"))
+ (duration 5))
+ (message "Recording system audio for %d seconds... PLAY SOMETHING NOW!" duration)
+ (shell-command
+ (format "ffmpeg -f pulse -i %s -t %d -y %s 2>/dev/null"
+ (shell-quote-argument cj/recording-system-device)
+ duration
+ (shell-quote-argument temp-file)))
+ (message "Playing back recording...")
+ (shell-command (format "ffplay -autoexit -nodisp %s 2>/dev/null &"
+ (shell-quote-argument temp-file)))
+ (message "Monitor test complete. Temp file: %s" temp-file)))
- ;; Validate devices
+(defun cj/recording-test-both ()
+ "Test both mic and monitor together with guided prompts.
+This simulates a real recording scenario:
+1. Tests mic only (speak into it)
+2. Tests monitor only (play audio/video)
+3. Tests both together (speak while audio plays)
+
+Run this before important recordings to verify everything works!"
+ (interactive)
(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"))
+ (user-error "Devices not configured. Run C-; r c first"))
+
+ (when (y-or-n-p "Test 1: Record from MICROPHONE only (5 sec). Ready? ")
+ (cj/recording-test-mic)
+ (sit-for 6)) ; Wait for playback
+
+ (when (y-or-n-p "Test 2: Record from SYSTEM AUDIO only (5 sec). Start playing audio/video, then press y: ")
+ (cj/recording-test-monitor)
+ (sit-for 6)) ; Wait for playback
+
+ (when (y-or-n-p "Test 3: Record BOTH mic + system audio (5 sec). Speak while audio plays, then press y: ")
+ (let* ((temp-file (make-temp-file "both-test-" nil ".wav"))
+ (duration 5))
+ (message "Recording BOTH for %d seconds... SPEAK + PLAY AUDIO NOW!" duration)
+ (shell-command
+ (format "ffmpeg -f pulse -i %s -f pulse -i %s -filter_complex \"[0:a]volume=%.1f[mic];[1:a]volume=%.1f[sys];[mic][sys]amix=inputs=2:duration=longest\" -t %d -y %s 2>/dev/null"
+ (shell-quote-argument cj/recording-mic-device)
+ (shell-quote-argument cj/recording-system-device)
+ cj/recording-mic-boost
+ cj/recording-system-volume
+ duration
+ (shell-quote-argument temp-file)))
+ (message "Playing back recording...")
+ (shell-command (format "ffplay -autoexit -nodisp %s 2>/dev/null &"
+ (shell-quote-argument temp-file)))
+ (sit-for 6)
+ (message "All tests complete! Temp file: %s" temp-file)))
+
+ (message "Device testing complete. If you heard audio in all tests, recording will work!"))
+
+(defun cj/recording-get-devices ()
+ "Get audio devices, prompting user if not already configured.
+Returns (mic-device . system-device) or nil on error."
+ ;; If devices not set, prompt user to select them
+ (unless (and cj/recording-mic-device cj/recording-system-device)
+ (if (y-or-n-p "Audio devices not configured. Use quick setup for calls? ")
+ (cj/recording-quick-setup-for-calls)
+ (cj/recording-select-devices)))
+
+ ;; Final validation
+ (unless (and cj/recording-mic-device cj/recording-system-device)
+ (user-error "Audio devices not configured. Run C-; r c (quick setup) or C-; r s (manual select)"))
(cons cj/recording-mic-device cj/recording-system-device))
-(defun cj/video-recording-start (arg)
- "Start the ffmpeg video recording.
+(defun cj/video-recording-toggle (arg)
+ "Toggle video recording: start if not recording, stop if recording.
+On first use (or when devices not configured), runs quick setup (C-; r c).
With prefix ARG, prompt for recording location.
Otherwise use the default location in `video-recordings-dir'."
(interactive "P")
- (let* ((location (if arg
- (read-directory-name "Enter recording location: ")
- video-recordings-dir))
- (directory (file-name-directory location)))
- (unless (file-directory-p directory)
- (make-directory directory t))
- (cj/ffmpeg-record-video location)))
-
-(defun cj/audio-recording-start (arg)
- "Start the ffmpeg audio recording.
+ (if cj/video-recording-ffmpeg-process
+ ;; Recording in progress - stop it
+ (cj/video-recording-stop)
+ ;; Not recording - start it
+ (let* ((location (if arg
+ (read-directory-name "Enter recording location: ")
+ video-recordings-dir))
+ (directory (file-name-directory location)))
+ (unless (file-directory-p directory)
+ (make-directory directory t))
+ (cj/ffmpeg-record-video location))))
+
+(defun cj/audio-recording-toggle (arg)
+ "Toggle audio recording: start if not recording, stop if recording.
+On first use (or when devices not configured), runs quick setup (C-; r c).
With prefix ARG, prompt for recording location.
Otherwise use the default location in `audio-recordings-dir'."
(interactive "P")
- (let* ((location (if arg
- (read-directory-name "Enter recording location: ")
- audio-recordings-dir))
- (directory (file-name-directory location)))
- (unless (file-directory-p directory)
- (make-directory directory t))
- (cj/ffmpeg-record-audio location)))
+ (if cj/audio-recording-ffmpeg-process
+ ;; Recording in progress - stop it
+ (cj/audio-recording-stop)
+ ;; Not recording - start it
+ (let* ((location (if arg
+ (read-directory-name "Enter recording location: ")
+ audio-recordings-dir))
+ (directory (file-name-directory location)))
+ (unless (file-directory-p directory)
+ (make-directory directory t))
+ (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))
@@ -140,41 +486,54 @@ Otherwise use the default location in `audio-recordings-dir'."
"*ffmpeg-video-recording*"
ffmpeg-command))
(set-process-query-on-exit-flag cj/video-recording-ffmpeg-process nil)
+ (set-process-sentinel cj/video-recording-ffmpeg-process #'cj/recording-process-sentinel)
+ (force-mode-line-update t)
(message "Started video recording to %s (mic: %.1fx, system: %.1fx)."
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.
+Records from microphone and system audio monitor (configured device), mixing them together.
+Use C-; r c to configure which device to use - it must match the device your phone call uses."
(cj/recording-check-ffmpeg)
(unless cj/audio-recording-ffmpeg-process
(let* ((devices (cj/recording-get-devices))
(mic-device (car devices))
+ ;; Use the explicitly configured monitor device
+ ;; This must match the device your phone call/audio is using
(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 "
+ "-f pulse -i %s " ; Input 0: Microphone (specific device)
+ "-f pulse -i %s " ; Input 1: System audio monitor
+ "-filter_complex \""
+ "[0:a]volume=%.1f[mic];" ; Apply mic boost
+ "[1:a]volume=%.1f[sys];" ; Apply system volume
+ "[mic][sys]amix=inputs=2:duration=longest[out]\" " ; Mix both inputs
+ "-map \"[out]\" "
+ "-c:a aac "
+ "-b:a 64k "
"%s")
mic-device
system-device
cj/recording-mic-boost
cj/recording-system-volume
filename)))
+ ;; Log the command for debugging
+ (message "Recording from mic: %s + ALL system outputs" mic-device)
+ (cj/log-silently "Audio recording ffmpeg command: %s" ffmpeg-command)
;; start the recording
(setq cj/audio-recording-ffmpeg-process
(start-process-shell-command "ffmpeg-audio-recording"
"*ffmpeg-audio-recording*"
ffmpeg-command))
(set-process-query-on-exit-flag cj/audio-recording-ffmpeg-process nil)
- (message "Started audio recording to %s (mic: %.1fx, system: %.1fx)."
+ (set-process-sentinel cj/audio-recording-ffmpeg-process #'cj/recording-process-sentinel)
+ (force-mode-line-update t)
+ (message "Started recording to %s (mic: %.1fx, all system audio: %.1fx)"
filename cj/recording-mic-boost cj/recording-system-volume))))
(defun cj/video-recording-stop ()
@@ -187,6 +546,7 @@ Otherwise use the default location in `audio-recordings-dir'."
;; Give ffmpeg a moment to finalize the file
(sit-for 0.2)
(setq cj/video-recording-ffmpeg-process nil)
+ (force-mode-line-update t)
(message "Stopped video recording."))
(message "No video recording in progress.")))
@@ -200,6 +560,7 @@ Otherwise use the default location in `audio-recordings-dir'."
;; Give ffmpeg a moment to finalize the file
(sit-for 0.2)
(setq cj/audio-recording-ffmpeg-process nil)
+ (force-mode-line-update t)
(message "Stopped audio recording."))
(message "No audio recording in progress.")))
@@ -217,15 +578,37 @@ Otherwise use the default location in `audio-recordings-dir'."
;; Recording operations prefix and keymap
(defvar cj/record-map
(let ((map (make-sparse-keymap)))
- (define-key map (kbd "V") #'cj/video-recording-stop)
- (define-key map (kbd "v") #'cj/video-recording-start)
- (define-key map (kbd "A") #'cj/audio-recording-stop)
- (define-key map (kbd "a") #'cj/audio-recording-start)
+ (define-key map (kbd "v") #'cj/video-recording-toggle)
+ (define-key map (kbd "a") #'cj/audio-recording-toggle)
(define-key map (kbd "l") #'cj/recording-adjust-volumes)
+ (define-key map (kbd "d") #'cj/recording-list-devices)
+ (define-key map (kbd "w") #'cj/recording-show-active-audio) ; "w" for "what's playing"
+ (define-key map (kbd "s") #'cj/recording-select-devices)
+ (define-key map (kbd "c") #'cj/recording-quick-setup-for-calls)
+ (define-key map (kbd "t m") #'cj/recording-test-mic)
+ (define-key map (kbd "t s") #'cj/recording-test-monitor)
+ (define-key map (kbd "t b") #'cj/recording-test-both)
map)
"Keymap for video/audio recording operations.")
-(keymap-set cj/custom-keymap "r" cj/record-map)
+;; Only set keybinding if cj/custom-keymap is bound (not in batch mode)
+(when (boundp 'cj/custom-keymap)
+ (keymap-set cj/custom-keymap "r" cj/record-map))
+
+(with-eval-after-load 'which-key
+ (which-key-add-key-based-replacements
+ "C-; r" "recording menu"
+ "C-; r v" "toggle video recording"
+ "C-; r a" "toggle audio recording"
+ "C-; r l" "adjust levels"
+ "C-; r d" "list devices"
+ "C-; r w" "what's playing (diagnostics)"
+ "C-; r s" "select devices"
+ "C-; r c" "quick setup for calls"
+ "C-; r t" "test devices"
+ "C-; r t m" "test microphone"
+ "C-; r t s" "test system audio"
+ "C-; r t b" "test both (guided)"))
(provide 'video-audio-recording)
;;; video-audio-recording.el ends here.