#Requires AutoHotkey v2.0+ #SingleInstance Force #Warn All, Off ; ============================================================================== ; Script Name: Call Recorder for FFmpeg ; Synopsis: ; This script manages audio recordings using FFmpeg. It captures audio from a ; specified microphone and saves recordings with timestamped filenames. ; The user can control recording via a system tray menu or hotkeys: ; - Ctrl+Alt+R: Start recording ; - Ctrl+Alt+S: Stop recording ; - Ctrl+Alt+X: Exit the application gracefully ; ; Additional Features: ; - Automatically creates a log directory if it doesn’t exist. ; - Verifies the FFmpeg executable location. ; - Ensures only one instance is running. ; - Provides clear tooltips for user feedback. ; ; Prerequisites: ; - AutoHotkey v2.0+ ; - FFmpeg installed at the specified location. ; - Correct microphone name matching the system device. ; ============================================================================== ; -------------------------------------------------------------- ; CONFIGURATION ; -------------------------------------------------------------- class Config { static FFmpegPath := "C:\Users\J2897\Programs\ffmpeg\bin\ffmpeg.exe" static MicName := "Microphone (Scarlett 2i2 USB)" static CallsDir := A_ScriptDir "\Calls" ; New directory static OutputDir := this.CallsDir ; Alias for clarity static LogDir := A_ScriptDir "\Logs" static StartDelay := 2000 } ; -------------------------------------------------------------- ; APPLICATION STATE ; -------------------------------------------------------------- class State { static ffmpegPID := 0 static IsRecording => this.ffmpegPID != 0 && ProcessExist(this.ffmpegPID) static Clear() { this.ffmpegPID := 0 } } ; -------------------------------------------------------------- ; TRAY MENU ; -------------------------------------------------------------- CreateTrayMenu() { A_TrayMenu.Delete() A_TrayMenu.Add("&Start Recording", (*) => StartRecording()) A_TrayMenu.Add("&Stop Recording", (*) => StopRecording()) A_TrayMenu.Add() A_TrayMenu.Add("E&xit", (*) => ExitAppGracefully()) UpdateTrayMenu() } UpdateTrayMenu() { ; Use separate Enable/Disable methods if State.IsRecording { A_TrayMenu.Disable("&Start Recording") A_TrayMenu.Enable("&Stop Recording") } else { A_TrayMenu.Enable("&Start Recording") A_TrayMenu.Disable("&Stop Recording") } } ; -------------------------------------------------------------- ; HOTKEYS ; -------------------------------------------------------------- ^!R:: StartRecording() ^!S:: StopRecording() ^!X:: ExitAppGracefully() ; -------------------------------------------------------------- ; INITIALIZATION ; -------------------------------------------------------------- CreateTrayMenu() ShowToolTip("Call Recorder Initialized." . "`nRight-click the tray icon for options." . "`nHotkeys available:" . "`n Ctrl+Alt+R: Start Recording" . "`n Ctrl+Alt+S: Stop Recording" . "`n Ctrl+Alt+X: Exit", 1, 5000) return ; -------------------------------------------------------------- ; FUNCTIONS ; -------------------------------------------------------------- StartRecording() { if State.IsRecording { ShowToolTip("Recording already in progress!", 2, 2000) return } if !FileExist(Config.FFmpegPath) { MsgBox("FFmpeg not found at:`n" Config.FFmpegPath, "Error", 0x30) return } ; Ensure both Logs and Calls directories exist if !DirExist(Config.LogDir) DirCreate(Config.LogDir) if !DirExist(Config.CallsDir) ; <-- Added this check DirCreate(Config.CallsDir) timestamp := FormatTime(, "yyyy-MM-dd_HH-mm-ss") outputFile := Config.OutputDir "\Call_" timestamp ".mp3" cmd := Format( 'cmd /c ""{1}" -hide_banner -f dshow -i audio="{2}" ' '-acodec libmp3lame -b:a 192k -ac 2 -y "{3}" & pause"', Config.FFmpegPath, Config.MicName, outputFile ) try { State.ffmpegPID := Run(cmd, Config.OutputDir,, &pid) ; Wait up to 5 seconds for FFmpeg to initialize startTime := A_TickCount while (A_TickCount - startTime < 5000) { if ProcessExist(pid) && FileExist(outputFile) break Sleep 500 } if !ProcessExist(pid) || !FileExist(outputFile) throw Error("FFmpeg failed to initialize") State.ffmpegPID := pid UpdateTrayMenu() } catch Error as e { State.Clear() MsgBox("Startup Error: " e.Message, "Error", 0x30) return } ShowToolTip("RECORDING STARTED (PID: " State.ffmpegPID ")", 2, 2000) UpdateTrayMenu() } StopRecording() { if !State.IsRecording { ShowToolTip("No active recording to stop", 2, 2000) return } ShowToolTip("Stopping recording...", 2, 2000) try { ; Force focus to FFmpeg's console WinActivate("ahk_pid " State.ffmpegPID) WinWaitActive("ahk_pid " State.ffmpegPID,, 1) ; Wait 1 sec for activation Send "q" ; Send 'q' directly to the active window Sleep 500 ; Give FFmpeg time to process the command if ProcessWaitClose(State.ffmpegPID, 2) { ShowToolTip("Stopped gracefully", 2, 2000) } else { ProcessClose(State.ffmpegPID) ShowToolTip("Force-closed recording", 2, 2000) } } catch Error as e { MsgBox("Stop Error: " e.Message, "Error", 0x30) } State.Clear() UpdateTrayMenu() } ExitAppGracefully() { if State.IsRecording { result := MsgBox("Recording in progress. Stop and exit?", "Confirm Exit", 0x34) if result = "Yes" StopRecording() else return } ExitApp() } ShowToolTip(text, id := 1, timeout := 0) { ToolTip(text,,, id) if timeout > 0 SetTimer(() => ToolTip(,,, id), -timeout) }