aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-01 13:45:19 -0400
committerCraig Jennings <c@cjennings.net>2026-07-01 13:45:19 -0400
commite46376a2d9263ba9c8ef26891de44bd5920bd4a9 (patch)
treea4c9e52ae6d1d1e9b028f691a3b7822442134ada
parent5a887435102236ac8381d24db5c75c02b9109276 (diff)
downloaddotemacs-e46376a2d9263ba9c8ef26891de44bd5920bd4a9.tar.gz
dotemacs-e46376a2d9263ba9c8ef26891de44bd5920bd4a9.zip
fix(recording): record audio-only to lossless FLAC, not AAC/M4A
Audio-only recordings were written as AAC in an MP4/.m4a container. The stop path SIGINTs ffmpeg, and if the MP4 muxer doesn't write its moov trailer before exit, the file has no moov atom and won't decode. ffmpeg and AssemblyAI both reject it. Three recordings were lost that way and had to be rebuilt with untrunc. The video path already avoids this by using Matroska, which needs no finalize pass. I switched the audio-only path to FLAC. FLAC frames are self-contained, so an abruptly stopped recording still decodes, with no trailer to miss at close. It's also lossless, dropping the 64k AAC encode that degraded speech before transcription. AssemblyAI recommends a lossless source and accepts FLAC directly. The transcription path passes audio files through untouched.
-rw-r--r--modules/video-audio-recording-capture.el24
-rw-r--r--tests/test-video-audio-recording--build-audio-command.el13
-rw-r--r--tests/test-video-audio-recording-command-structure.el6
-rw-r--r--tests/test-video-audio-recording-ffmpeg-functions.el6
4 files changed, 29 insertions, 20 deletions
diff --git a/modules/video-audio-recording-capture.el b/modules/video-audio-recording-capture.el
index 069975bc..ea0d687c 100644
--- a/modules/video-audio-recording-capture.el
+++ b/modules/video-audio-recording-capture.el
@@ -229,7 +229,12 @@ On X11: ffmpeg captures screen directly via x11grab with PulseAudio audio."
(defun cj/recording--build-audio-command (mic-device system-device filename)
"Build the ffmpeg shell command string for audio-only recording.
MIC-DEVICE and SYSTEM-DEVICE are PulseAudio device names. FILENAME is
-the output .m4a path. Mixes mic + system monitor into a single AAC file."
+the output .flac path. Mixes mic + system monitor into a single lossless
+FLAC file. FLAC is used instead of AAC/M4A for two reasons: it is
+lossless (no encoder-quality loss, which matters for transcription
+accuracy), and its frames are self-contained so an abruptly stopped
+recording is still decodable -- unlike MP4, which needs a moov trailer
+written at close."
(format (concat "ffmpeg "
"-f pulse -i %s " ; Input 0: microphone
"-f pulse -i %s " ; Input 1: system audio monitor
@@ -238,8 +243,7 @@ the output .m4a path. Mixes mic + system monitor into a single AAC file."
"[1:a]volume=%.1f[sys];"
"[mic][sys]amix=inputs=2:duration=longest[out]\" "
"-map \"[out]\" "
- "-c:a aac "
- "-b:a 64k "
+ "-c:a flac "
"%s")
(shell-quote-argument mic-device)
(shell-quote-argument system-device)
@@ -287,10 +291,10 @@ Uses wf-recorder on Wayland, x11grab on X11."
(defun cj/ffmpeg-record-audio (directory)
"Start an audio recording, saving output to DIRECTORY.
Records from microphone and system audio monitor (configured device),
-mixing them together into a single M4A/AAC file.
+mixing them together into a single lossless FLAC file.
The filter graph mixes two PulseAudio inputs:
- [mic] → volume boost → amerge → AAC encoder → .m4a
+ [mic] → volume boost → amix → FLAC encoder → .flac
[sys] → volume boost ↗"
(cj/recording-check-ffmpeg)
(unless cj/audio-recording-ffmpeg-process
@@ -299,7 +303,7 @@ The filter graph mixes two PulseAudio inputs:
(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 ".m4a") location))
+ (filename (expand-file-name (concat name ".flac") location))
(ffmpeg-command
(cj/recording--build-audio-command mic-device system-device filename)))
(message "Recording from mic: %s + ALL system outputs" mic-device)
@@ -370,7 +374,7 @@ for ffmpeg to write container metadata before giving up."
(defun cj/audio-recording-stop ()
"Stop the audio recording, waiting for ffmpeg to finalize the file.
Sends SIGINT to the process group and waits up to 3 seconds for ffmpeg
-to flush audio frames and write the M4A container trailer."
+to flush audio frames and finalize the FLAC stream."
(interactive)
(if (not cj/audio-recording-ffmpeg-process)
(message "No audio recording in progress.")
@@ -379,8 +383,10 @@ to flush audio frames and write the M4A container trailer."
(let ((pid (process-id proc)))
(when pid
(signal-process (- pid) 2)))
- ;; M4A finalization is faster than MKV, but still needs time to write
- ;; the AAC trailer and flush the output buffer.
+ ;; On a clean stop ffmpeg seeks back and backfills the FLAC STREAMINFO
+ ;; (total samples, MD5) so duration reads correctly. Even a hard
+ ;; truncation leaves a decodable file, since FLAC frames are
+ ;; self-contained -- there is no end-of-file trailer to miss.
(let ((exited (cj/recording--wait-for-exit proc 3)))
(unless exited
(message "Warning: recording process did not exit within 3 seconds")))
diff --git a/tests/test-video-audio-recording--build-audio-command.el b/tests/test-video-audio-recording--build-audio-command.el
index 54e5f56c..9577899b 100644
--- a/tests/test-video-audio-recording--build-audio-command.el
+++ b/tests/test-video-audio-recording--build-audio-command.el
@@ -3,8 +3,8 @@
;;; Commentary:
;; Unit tests for cj/recording--build-audio-command.
;; Verifies correct ffmpeg command construction for audio-only recording
-;; (mic + system monitor mixed to M4A/AAC), including shell quoting of
-;; device names and output paths.
+;; (mic + system monitor mixed to lossless FLAC), including shell quoting
+;; of device names and output paths.
;;; Code:
@@ -19,14 +19,17 @@
;;; Normal Cases
(ert-deftest test-video-audio-recording--build-audio-command-normal-uses-ffmpeg-pulse ()
- "Normal: audio command uses ffmpeg with two PulseAudio inputs mixed to AAC."
+ "Normal: audio command uses ffmpeg with two PulseAudio inputs mixed to FLAC."
(let ((cj/recording-mic-boost 2.0)
(cj/recording-system-volume 1.0))
- (let ((cmd (cj/recording--build-audio-command "mic" "sys" "/tmp/out.m4a")))
+ (let ((cmd (cj/recording--build-audio-command "mic" "sys" "/tmp/out.flac")))
(should (string-match-p "ffmpeg" cmd))
(should (string-match-p "-f pulse -i" cmd))
(should (string-match-p "amix=inputs=2" cmd))
- (should (string-match-p "-c:a aac" cmd)))))
+ (should (string-match-p "-c:a flac" cmd))
+ ;; Lossless: no lossy bitrate cap should be emitted.
+ (should-not (string-match-p "-b:a" cmd))
+ (should-not (string-match-p "-c:a aac" cmd)))))
(ert-deftest test-video-audio-recording--build-audio-command-normal-devices-in-command ()
"Normal: both mic and system device names appear in the command."
diff --git a/tests/test-video-audio-recording-command-structure.el b/tests/test-video-audio-recording-command-structure.el
index f4c24c39..41bd3b6a 100644
--- a/tests/test-video-audio-recording-command-structure.el
+++ b/tests/test-video-audio-recording-command-structure.el
@@ -260,8 +260,8 @@
(should (string-match-p "-f pulse" command))))
(test-command-structure-teardown)))
-(ert-deftest test-audio-recording-command-outputs-m4a ()
- "Test that audio recording outputs to .m4a file."
+(ert-deftest test-audio-recording-command-outputs-flac ()
+ "Test that audio recording outputs to .flac file."
(test-command-structure-setup)
(unwind-protect
(let ((command nil))
@@ -272,7 +272,7 @@
((symbol-function 'cj/recording--validate-system-audio)
(lambda () nil)))
(cj/ffmpeg-record-audio audio-recordings-dir)
- (should (string-match-p "\\.m4a" command))))
+ (should (string-match-p "\\.flac" command))))
(test-command-structure-teardown)))
;;; Common Command Structure (Both Video and Audio)
diff --git a/tests/test-video-audio-recording-ffmpeg-functions.el b/tests/test-video-audio-recording-ffmpeg-functions.el
index 4b3570a2..3a38d00a 100644
--- a/tests/test-video-audio-recording-ffmpeg-functions.el
+++ b/tests/test-video-audio-recording-ffmpeg-functions.el
@@ -164,8 +164,8 @@
(should update-called)))
(test-ffmpeg-teardown)))
-(ert-deftest test-video-audio-recording-ffmpeg-record-audio-normal-creates-m4a-file ()
- "Test that audio recording creates .m4a file."
+(ert-deftest test-video-audio-recording-ffmpeg-record-audio-normal-creates-flac-file ()
+ "Test that audio recording creates .flac file."
(test-ffmpeg-setup)
(unwind-protect
(let ((command nil))
@@ -176,7 +176,7 @@
((symbol-function 'cj/recording--validate-system-audio)
(lambda () nil)))
(cj/ffmpeg-record-audio audio-recordings-dir)
- (should (string-match-p "\\.m4a" command))))
+ (should (string-match-p "\\.flac" command))))
(test-ffmpeg-teardown)))
;;; Stop Functions - Normal Cases