diff options
| -rw-r--r-- | docs/sessions/refactor.org | 432 | ||||
| -rw-r--r-- | modules/video-audio-recording.el | 12 |
2 files changed, 234 insertions, 210 deletions
diff --git a/docs/sessions/refactor.org b/docs/sessions/refactor.org index 11ff0a91..0cdb6841 100644 --- a/docs/sessions/refactor.org +++ b/docs/sessions/refactor.org @@ -1,14 +1,14 @@ -#+TITLE: Test-Driven Quality Engineering Session: music-config.el +#+TITLE: Test-Driven Quality Engineering Session Process #+AUTHOR: Craig Jennings & Claude #+DATE: 2025-11-01 * Overview -This document describes a comprehensive test-driven quality engineering session for =music-config.el=, an EMMS music player configuration module. The session demonstrates systematic testing practices, refactoring for testability, bug discovery through tests, and decision-making processes when tests fail. +This document describes a comprehensive test-driven quality engineering session process applicable to any source code module. The session demonstrates systematic testing practices, refactoring for testability, bug discovery through tests, and decision-making processes when tests fail. * Session Goals -1. Add comprehensive unit test coverage for testable functions in =music-config.el= +1. Add comprehensive unit test coverage for testable functions in your module 2. Discover and fix bugs through systematic testing 3. Follow quality engineering principles from =ai-prompts/quality-engineer.org= 4. Demonstrate refactoring patterns for testability @@ -18,12 +18,12 @@ This document describes a comprehensive test-driven quality engineering session ** The Feature Request -Add functionality to append a track from the EMMS playlist to an existing M3U file by pressing ~A~ on the track. +Add new functionality that requires user interaction combined with business logic. -Requirements: -- Show completing-read with available M3U playlists -- Allow cancellation (C-g and explicit "(Cancel)" option) -- Append track's absolute path to selected M3U +Example requirements: +- Present user with options (e.g., interactive selection) +- Allow cancellation +- Perform an operation with the selected input - Provide clear success/failure feedback ** Refactoring for Testability @@ -31,26 +31,26 @@ Requirements: Following the "Interactive vs Non-Interactive Function Pattern" from =quality-engineer.org=: *Problem:* Directly implementing as an interactive function would require: -- Mocking =completing-read= -- Mocking =emms-playlist-track-at= -- Testing Emacs UI functionality, not our business logic +- Mocking user interface components +- Mocking framework-specific APIs +- Testing UI functionality, not core business logic *Solution:* Split into two functions: -1. *Helper Function* (=cj/music--append-track-to-m3u-file=): +1. *Helper Function* (internal implementation): - Pure, deterministic - - Takes explicit parameters: =(track-path m3u-file)= + - Takes explicit parameters - No user interaction - - Returns =t= on success, signals errors naturally - - 100% testable with ERT, no mocking needed + - Returns values or signals errors naturally + - 100% testable, no mocking needed -2. *Interactive Wrapper* (=cj/music-append-track-to-playlist=): +2. *Interactive Wrapper* (public interface): - Thin layer handling only user interaction - - Gets track at point - - Shows completing-read + - Gets input from user/context + - Presents UI (prompts, selections, etc.) - Catches errors and displays messages - Delegates all business logic to helper - - No tests needed (just testing Emacs) + - No tests needed (just testing framework UI) ** Benefits of This Pattern @@ -58,13 +58,13 @@ From =quality-engineer.org=: #+begin_quote When writing functions that combine business logic with user interaction: - Split into internal implementation and interactive wrapper -- Internal function (prefix with =--=): Pure logic, takes all parameters explicitly +- Internal function: Pure logic, takes all parameters explicitly - Dramatically simpler testing (no interactive mocking) - Code reusable programmatically without prompts - Clear separation of concerns (logic vs UI) #+end_quote -This pattern enabled: +This pattern enables: - Zero mocking in tests - Fast, deterministic tests - Easy reasoning about correctness @@ -72,303 +72,324 @@ This pattern enabled: * Phase 2: Writing the First Test -** Test File: =test-music-config--append-track-to-m3u-file.el= +** Test File Naming Following the naming convention from =quality-engineer.org=: -- Pattern: =test-<module>-<function>.el= +- Pattern: =test-<module>-<function>.<ext>= - One test file per function for easy discovery when tests fail -- User sees failure → immediately knows which file to open +- Developer sees failure → immediately knows which file to open ** Test Organization Following the three-category structure: -*** Normal Cases (4 tests) -- Append to empty file -- Append to file with trailing newline -- Append to file without trailing newline (adds leading newline) -- Multiple appends (allows duplicates) +*** Normal Cases +- Standard expected inputs +- Common use case scenarios +- Happy path operations +- Multiple operations in sequence -*** Boundary Cases (4 tests) -- Very long paths (~500 chars) +*** Boundary Cases +- Very long inputs - Unicode characters (中文, emoji) -- Spaces and special characters -- M3U with comments/metadata +- Special characters and edge cases +- Empty or minimal data +- Maximum values -*** Error Cases (3 tests) -- Nonexistent file -- Read-only file -- Directory instead of file +*** Error Cases +- Invalid inputs +- Nonexistent resources +- Permission denied scenarios +- Wrong type of input ** Writing Tests with Zero Mocking Key principle: "Don't mock what you're testing" (from =quality-engineer.org=) -Example test: -#+begin_src elisp -(ert-deftest test-music-config--append-track-to-m3u-file-normal-empty-file-appends-track () - "Append to brand new empty M3U file." - (test-music-config--append-track-to-m3u-file-setup) - (unwind-protect - (let* ((m3u-file (cj/create-temp-test-file "test-playlist-")) - (track-path "/home/user/music/artist/song.mp3")) - (cj/music--append-track-to-m3u-file track-path m3u-file) - (with-temp-buffer - (insert-file-contents m3u-file) - (should (string= (buffer-string) (concat track-path "\n"))))) - (test-music-config--append-track-to-m3u-file-teardown))) +Example test structure: +#+begin_src +test_function_normal_case_expected_result() + setup() + try: + # Arrange + input_data = create_test_data() + expected_output = define_expected_result() + + # Act + actual_output = function_under_test(input_data) + + # Assert + assert actual_output == expected_output + finally: + teardown() #+end_src -Notice: -- No mocks -- Real file I/O using =testutil-general.el= helpers +Key characteristics: +- No mocks for the function being tested +- Real resources (files, data structures) using test utilities - Tests actual function behavior - Clean setup/teardown +- Clear arrange-act-assert structure ** Result -All 11 tests passed on first run. The pure, deterministic helper function worked correctly. +When helper functions are well-factored and deterministic, tests often pass on first run. * Phase 3: Systematic Test Coverage Analysis ** Identifying Testable Functions -Reviewed all functions in =music-config.el= and categorized: +Review all functions in your module and categorize by testability: *** Easy to Test (Pure/Deterministic) -- =cj/music--valid-file-p= - Extension validation -- =cj/music--valid-directory-p= - Directory validation -- =cj/music--safe-filename= - String sanitization -- =cj/music--m3u-file-tracks= - M3U file parsing -- =cj/music--get-m3u-basenames= - Basename extraction - -*** Medium Complexity (Need File I/O) -- =cj/music--collect-entries-recursive= - Recursive directory traversal -- =cj/music--get-m3u-files= - File discovery -- =cj/music--completion-table= - Completion table generation - -*** Hard to Test (EMMS Buffer Dependencies) -- =cj/music--ensure-playlist-buffer= - EMMS buffer creation -- =cj/music--playlist-tracks= - EMMS buffer reading -- =cj/music--playlist-modified-p= - EMMS buffer state -- =cj/music--assert-valid-playlist-file= - Buffer-local state - -*Decision:* Test easy and medium complexity functions. Skip EMMS-dependent functions (would require extensive mocking/setup, diminishing returns). +- Input validation functions +- String manipulation/formatting +- Data structure transformations +- File parsing (read-only operations) +- Configuration/option processing + +*** Medium Complexity (Need External Resources) +- File I/O operations +- Recursive algorithms +- Data structure generation +- Cache or state management + +*** Hard to Test (Framework/Context Dependencies) +- Functions requiring specific runtime environment +- UI/buffer/window management +- Functions tightly coupled to framework internals +- Functions requiring complex mocking setup + +*Decision:* Test easy and medium complexity functions. Skip framework-dependent functions that would require extensive mocking/setup (diminishing returns). ** File Organization Principle From =quality-engineer.org=: #+begin_quote *Unit Tests*: One file per method -- Naming: =test-<filename>-<methodname>.el= -- Example: =test-org-gcal--safe-substring.el= +- Naming: =test-<filename>-<methodname>.<ext>= +- Example: =test-module--function.ext= #+end_quote *Rationale:* When a test fails in CI: -1. Developer sees: =test-music-config--get-m3u-files-normal-multiple-files-returns-list FAILED= -2. Immediately knows: Look for =test-music-config--get-m3u-files.el= +1. Developer sees: =test-module--function-normal-case-returns-result FAILED= +2. Immediately knows: Look for =test-module--function.<ext>= 3. Opens file and fixes issue - *fast cognitive path* If combined files: -1. Test fails: =test-music-config--get-m3u-files-normal-multiple-files-returns-list FAILED= -2. Which file? =test-music-config--m3u-helpers.el=? =test-music-config--combined.el=? +1. Test fails: =test-module--function-normal-case-returns-result FAILED= +2. Which file? =test-module--helpers.<ext>=? =test-module--combined.<ext>=? 3. Developer wastes time searching - *slower, frustrating* *The 1:1 mapping is a usability feature for developers under pressure.* * Phase 4: Testing Function by Function -** Function 1: =cj/music--valid-file-p= +** Example 1: Input Validation Function *** Test Categories *Normal Cases:* -- Valid extensions (mp3, flac, etc.) -- Case-insensitive matching (MP3, Mp3) +- Valid inputs +- Case variations +- Common use cases *Boundary Cases:* -- Dots in path (only last extension matters) -- Multiple extensions (uses rightmost) -- No extension -- Empty string +- Edge cases in input format +- Multiple delimiters or separators +- Empty or minimal input +- Very long input *Error Cases:* -- Nil input -- Non-music extensions +- Nil/null input +- Wrong type +- Malformed input -*** First Run: 14/15 Passed, 1 FAILED +*** First Run: Most Passed, Some FAILED -*Failure:* +*Example Failure:* #+begin_src -test-music-config--valid-file-p-error-nil-input-returns-nil +test-module--validate-input-error-nil-input-returns-nil Expected: Returns nil gracefully -Actual: (wrong-type-argument stringp nil) - CRASHED +Actual: (TypeError/NullPointerException) - CRASHED #+end_src *** Bug Analysis: Test or Production Code? *Process:* -1. Read the test expectation: "nil input returns nil gracefully" +1. Read the test expectation: "nil input returns nil/false gracefully" 2. Read the production code: - #+begin_src elisp - (defun cj/music--valid-file-p (file) - (when-let ((ext (file-name-extension file))) ; ← Crashes here - (member (downcase ext) cj/music-file-extensions))) + #+begin_src + function validate_input(input): + extension = get_extension(input) # ← Crashes here on nil/null + return extension in valid_extensions #+end_src -3. Identify issue: =file-name-extension= expects string, crashes on nil +3. Identify issue: Function expects string, crashes on nil/null 4. Consider context: This is defensive validation code, called in various contexts *Decision: Fix production code* *Rationale:* - Function should be defensive (validation code) -- Returning nil for invalid input is more robust than crashing -- Common pattern in Emacs Lisp validation functions +- Returning false/nil for invalid input is more robust than crashing +- Common pattern in validation functions +- Better user experience *Fix:* -#+begin_src elisp -(defun cj/music--valid-file-p (file) - (when (and file (stringp file)) ; ← Guard added - (when-let ((ext (file-name-extension file))) - (member (downcase ext) cj/music-file-extensions)))) +#+begin_src +function validate_input(input): + if input is None or not isinstance(input, str): # ← Guard added + return False + extension = get_extension(input) + return extension in valid_extensions #+end_src -Result: All 15 tests passed. +Result: All tests pass after adding defensive checks. -** Function 2: =cj/music--valid-directory-p= +** Example 2: Another Validation Function -*** First Run: 11/13 Passed, 2 FAILED +*** First Run: Most Passed, Multiple FAILED *Failures:* -1. Nil input crashed (same pattern as =valid-file-p=) -2. Empty string returned non-nil (treated as current directory) +1. Nil input crashed (same pattern as previous function) +2. Empty string returned unexpected value (edge case not handled) *Fix:* -#+begin_src elisp -(defun cj/music--valid-directory-p (dir) - (when (and dir (stringp dir) (not (string-empty-p dir))) ; ← Guards added - (and (file-directory-p dir) - (not (string-prefix-p "." (file-name-nondirectory - (directory-file-name dir))))))) +#+begin_src +function validate_resource(resource): + # Guards added for nil/null and empty string + if not resource or not isinstance(resource, str) or resource.strip() == "": + return False + + # Original validation logic + return is_valid_resource(resource) and meets_criteria(resource) #+end_src -Result: All 13 tests passed. +Result: All tests pass after adding comprehensive guards. -** Function 3: =cj/music--safe-filename= +** Example 3: String Sanitization Function -*** First Run: 12/13 Passed, 1 FAILED +*** First Run: Most Passed, 1 FAILED *Failure:* #+begin_src -test-music-config--safe-filename-boundary-special-chars-replaced -Expected: "playlist__________" (10 underscores) -Actual: "playlist_________" (9 underscores) +test-module--sanitize-boundary-special-chars-replaced +Expected: "output__________" (10 underscores) +Actual: "output_________" (9 underscores) #+end_src *** Bug Analysis: Test or Production Code? *Process:* -1. Count special chars in input: =@#$%^&*()= = 9 characters -2. Test expected 10, but input only has 9 -3. Production code is correct +1. Count special chars in test input: 9 characters +2. Test expected 10 replacements, but input only has 9 +3. Production code is working correctly *Decision: Fix test code* *The bug was in the test expectation, not the implementation.* -Result: All 13 tests passed. +Result: All tests pass after correcting test expectations. -** Function 4: =cj/music--m3u-file-tracks= (M3U Parser) +** Example 4: File/Data Parser Function -This is where we found a **significant bug** through testing! +This is where a **significant bug** was discovered through testing! *** Test Categories *Normal Cases:* -- Absolute paths -- Relative paths (expanded to M3U directory) -- HTTP/HTTPS/MMS URLs preserved -- Mixed paths and URLs +- Absolute paths/references +- Relative paths (expanded to base directory) +- URLs/URIs preserved as-is +- Mixed types of references *Boundary Cases:* -- Empty lines ignored (important for playlist robustness!) +- Empty lines ignored - Whitespace-only lines ignored -- Comments ignored (#EXTM3U, #EXTINF) +- Comments ignored (format-specific) - Leading/trailing whitespace trimmed - Order preserved *Error Cases:* - Nonexistent file -- Nil input +- Nil/null input -*** First Run: 11/15 Passed, 4 FAILED +*** First Run: Majority Passed, Multiple FAILED -All 4 failures related to URL handling: +All failures related to URL/URI handling: *Failure Pattern:* #+begin_src -Expected: "http://example.com/stream.mp3" -Actual: "/home/cjennings/.temp-emacs-tests/http:/example.com/stream.mp3" +Expected: "http://example.com/resource" +Actual: "/base/path/http:/example.com/resource" #+end_src -HTTP/HTTPS/MMS URLs were being treated as relative paths and mangled! +URLs were being treated as relative paths and corrupted! *** Root Cause Analysis -*Production code (line 110):* -#+begin_src elisp -(string-match-p "\`\(https?\|mms\)://" line) +*Production code:* +#+begin_src +if line.matches("^\(https?|mms\)://"): # Pattern detection + # Handle as URL #+end_src -*Problem:* Regex escaping is wrong! +*Problem:* Pattern matching is incorrect! -In the string literal ="\`"=: -- The backslash-backtick becomes a *literal backtick character* -- Not the regex anchor =\`= (start of string) +The pattern/regex has an error: +- Incorrect escaping or syntax +- Pattern fails to match valid URLs +- All URLs fall through to the "relative path" handler -The regex never matched, so URLs were treated as relative paths. +The pattern never matched, so URLs were incorrectly processed as relative paths. *Correct version:* -#+begin_src elisp -(string-match-p "\\`\\(https?\\|mms\\)://" line) +#+begin_src +if line.matches("^(https?|mms)://"): # Fixed pattern + # Handle as URL #+end_src -Double backslashes for string literal escaping → results in regex =\`\(https?\|mms\)://= +Common causes of this type of bug: +- String escaping issues in the language +- Incorrect regex syntax +- Copy-paste errors in patterns *** Impact Assessment *This is a significant bug:* -- Radio stations (HTTP streams) would be broken -- Any M3U with URLs would fail -- Data corruption: URLs transformed into nonsensical file paths -- Function worked for local files, so bug went unnoticed -- Users would see mysterious errors when loading playlists with streams +- Remote resources (URLs) would be broken +- Data corruption: URLs transformed into invalid paths +- Function worked for local/simple cases, so bug went unnoticed +- Users would see mysterious errors when using remote resources +- Potential data loss or corruption in production *Tests caught a real production bug that could have caused user data corruption!* -Result: All 15 tests passed after fix. +Result: All tests pass after fixing the pattern matching logic. * Phase 5: Continuing Through the Test Suite -** Functions Tested Successfully +** Additional Functions Tested Successfully + +As testing continues through the module, patterns emerge: -5. =cj/music--get-m3u-files= - 7 tests - - Learned: Directory listing order is filesystem-dependent +*Function: Directory/File Listing* + - Learning: Directory listing order may be filesystem-dependent - Solution: Sort results before comparing in tests -6. =cj/music--get-m3u-basenames= - 6 tests - - Kept as separate file (not combined with get-m3u-files) +*Function: Data Extraction* + - Keep as separate test file (don't combine with related functions) - Reason: Usability when tests fail -7. =cj/music--collect-entries-recursive= - 12 tests - - Medium complexity: Required creating test directory trees - - Used =testutil-general.el= helpers for setup/teardown - - All tests passed first time (well-factored function) +*Function: Recursive Operations* + - Medium complexity: Required creating test data structures/trees + - Use test utilities for setup/teardown + - Well-factored functions often pass all tests initially -8. =cj/music--completion-table= - 12 tests - - Tested higher-order function (returns lambda) - - Initially misunderstood completion protocol behavior - - Fixed test expectations to match actual Emacs behavior +*Function: Higher-Order Functions* + - Test functions that return functions/callbacks + - Initially may misunderstand framework/protocol behavior + - Fix test expectations to match actual framework behavior * Key Principles Applied @@ -396,9 +417,9 @@ This makes it easy to: Pattern: =test-<module>-<function>-<category>-<scenario>-<expected-result>= Examples: -- =test-music-config--valid-file-p-normal-mp3-extension-returns-true= -- =test-music-config--m3u-file-tracks-boundary-empty-lines-ignored= -- =test-music-config--safe-filename-error-nil-input-signals-error= +- =test-module--validate-input-normal-valid-extension-returns-true= +- =test-module--parse-data-boundary-empty-lines-ignored= +- =test-module--sanitize-error-nil-input-signals-error= Benefits: - Self-documenting @@ -473,36 +494,38 @@ Benefits: * Final Results -** Test Coverage +** Test Coverage Example + +*Multiple functions tested with comprehensive coverage:* +1. File operation helper - ~10-15 tests +2. Input validation function - ~15 tests +3. Resource validation function - ~13 tests +4. String sanitization function - ~13 tests +5. File/data parser function - ~15 tests +6. Directory listing function - ~7 tests +7. Data extraction function - ~6 tests +8. Recursive operation function - ~12 tests +9. Higher-order function - ~12 tests -*9 functions tested, 103 tests total:* -1. =cj/music--append-track-to-m3u-file= - 11 tests -2. =cj/music--valid-file-p= - 15 tests -3. =cj/music--valid-directory-p= - 13 tests -4. =cj/music--safe-filename= - 13 tests -5. =cj/music--m3u-file-tracks= - 15 tests -6. =cj/music--get-m3u-files= - 7 tests -7. =cj/music--get-m3u-basenames= - 6 tests -8. =cj/music--collect-entries-recursive= - 12 tests -9. =cj/music--completion-table= - 12 tests +Total: Comprehensive test suite covering all testable functions ** Bugs Discovered and Fixed -1. *=cj/music--valid-file-p=* - - Issue: Crashed on nil input - - Fix: Added nil/string guard +1. *Input Validation Function* + - Issue: Crashed on nil/null input + - Fix: Added nil/type guards - Impact: Prevents crashes in validation code -2. *=cj/music--valid-directory-p=* +2. *Resource Validation Function* - Issue: Crashed on nil, treated empty string as valid - Fix: Added guards for nil and empty string - - Impact: More robust directory validation + - Impact: More robust validation -3. *=cj/music--m3u-file-tracks=* ⚠️ *SIGNIFICANT BUG* - - Issue: URL regex escaping wrong - HTTP/HTTPS/MMS URLs mangled as relative paths - - Fix: Corrected regex escaping: ="\`"= → ="\\`"= - - Impact: Radio stations and streaming URLs now work correctly - - *This bug would have corrupted user data and broken streaming playlists* +3. *File/Data Parser Function* ⚠️ *SIGNIFICANT BUG* + - Issue: Pattern matching wrong - URLs/URIs corrupted as relative paths + - Fix: Corrected pattern matching logic + - Impact: Remote resources now work correctly + - *This bug would have corrupted user data in production* ** Code Quality Improvements @@ -517,9 +540,10 @@ Benefits: ** 1. Tests as Bug Discovery Tools Tests aren't just for preventing regressions - they actively *discover existing bugs*: -- The URL regex bug existed in production -- Nil handling bugs would have manifested in edge cases -- Tests made these issues visible immediately +- Pattern matching bugs may exist in production +- Nil/null handling bugs manifest in edge cases +- Tests make these issues visible immediately +- Bugs caught before users encounter them ** 2. Refactoring Enables Testing @@ -547,11 +571,11 @@ One test file per function: ** 5. Test Quality Equals Production Quality -Our tests: -- Used real I/O (not mocks) -- Tested actual behavior -- Covered edge cases systematically -- Found real bugs +Quality tests: +- Use real resources (not mocks) +- Test actual behavior +- Cover edge cases systematically +- Find real bugs This is only possible with well-factored, testable code. @@ -581,13 +605,13 @@ Note: =quality-engineer.org= evolves as we learn more quality best practices. Th * Conclusion -This session demonstrated how systematic testing combined with refactoring for testability can: +This session process demonstrates how systematic testing combined with refactoring for testability can: - Discover real bugs before they reach users - Improve code quality and robustness - Build confidence in changes - Create maintainable test suites - Follow industry best practices -The 103 tests and 3 bug fixes represent a significant quality improvement to =music-config.el=. The URL regex bug alone justified the entire testing effort - that bug could have caused user data corruption and broken a major feature (streaming radio). +A comprehensive test suite with multiple bug fixes represents significant quality improvement to any module. Critical bugs (like the pattern matching issue in the example) alone can justify the entire testing effort - such bugs can cause data corruption and break major features. *Testing is not just about preventing future bugs - it's about finding bugs that already exist.* diff --git a/modules/video-audio-recording.el b/modules/video-audio-recording.el index f8df2cb4..c714a0a6 100644 --- a/modules/video-audio-recording.el +++ b/modules/video-audio-recording.el @@ -70,7 +70,7 @@ Returns device name or nil if not found." (match-string 1 output)))) (defun cj/recording--parse-pactl-output (output) - "Internal parser for pactl sources output. Takes OUTPUT string. + "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)) @@ -203,7 +203,7 @@ Returns alist of (device-name . (mic-source . monitor-source))." (nreverse result))) (defun cj/recording-quick-setup-for-calls () - "Quick setup for recording calls/meetings. + "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." @@ -234,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)) @@ -270,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)) @@ -303,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)) |
