3097 字
15 分钟
自动化EAC音频抓取
EAC(Exact Audio Copy)广泛应用于CD抓轨中,EAC日志也已成为验证完美抓轨中不可缺少的一部分;且与XLD一样基本上成为精确抓轨的事实标准。但是EAC的配置和使用非常复杂,即使你了解所有步骤也有可能漏做几步,导致log达不到100分。本文不会讨论怎么正确的配置光驱和EAC相关的设置,这些设置是一次性的,可以看看这篇文章。当然,EAC版本需要升级到最新版,我没有在旧版EAC上测试过。
一个完整的抓轨流程需要以下步骤:
- 碟片进仓
- 检测间隙
- 检测静音间隙
- 生成非规则CUE
- 抓取已压缩文件
这四部所有的按钮都藏在菜单中,生成CUE的按钮甚至没有快捷键,这很不好。所以基于AutoHotkey,我写了一个脚本来自动化这个流程,点击一次按钮就能完成以上所有步骤。
当然,目前仅针对中文版本的最新的EAC(当前为1.8)进行适配,其他语言和版本未经测试,需要自己修改。这目前只能在一个EAC实例下工作,多EAC需要自己适配。放入碟盘,点击自动模式,然后等弹出就可以了。还有一个手动模式用来处理有隐藏轨或者质量不好的碟片。
; Copyright (c) 2026 Nullpinter; This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.; If a copy of the MPL was not distributed with this file, You can obtain one at https://mozilla.org/MPL/2.0/.748 collapsed lines
Persistent
SetTitleMatchMode 2DetectHiddenWindows false
global EAC_WIN := "ahk_exe EAC.exe"
global current_step := 1global is_running := falseglobal abort_flag := false
global ui := 0global title_text := 0global status_text := 0global next_text := 0global detail_text := 0global progress_bar := 0global next_btn := 0global auto_btn := 0global skip_btn := 0global reset_btn := 0global exit_btn := 0
InitUi()
InitUi() { global ui, title_text, status_text, next_text, detail_text global progress_bar, next_btn, auto_btn, skip_btn, reset_btn, exit_btn
ui := Gui("+AlwaysOnTop +ToolWindow", "EAC 自动化控制") ui.MarginX := 16 ui.MarginY := 14 ui.SetFont("s10", "Microsoft YaHei UI")
title_text := ui.AddText("w620 h28", "EAC 自动化控制") status_text := ui.AddText("w620 h32", "") next_text := ui.AddText("w620 h32", "") progress_bar := ui.AddProgress("w620 h16 Range0-100", 0)
detail_text := ui.AddEdit("w620 h150 ReadOnly -Wrap", "")
next_btn := ui.AddButton("xm w120 h34", "执行下一步") auto_btn := ui.AddButton("x+8 w120 h34", "全自动模式") skip_btn := ui.AddButton("x+8 w110 h34", "跳过当前步") reset_btn := ui.AddButton("x+8 w100 h34", "重置流程") exit_btn := ui.AddButton("x+8 w80 h34", "退出")
next_btn.OnEvent("Click", (*) => RunNextStep()) auto_btn.OnEvent("Click", (*) => RunAutoMode()) skip_btn.OnEvent("Click", (*) => SkipCurrentStep()) reset_btn.OnEvent("Click", (*) => ResetFlow()) exit_btn.OnEvent("Click", (*) => ExitApp())
ui.OnEvent("Close", (*) => ExitApp())
RefreshUi("等待操作", "请点击“执行下一步”,或点击“全自动模式”。", 0) ui.Show("AutoSize Center")}
RunNextStep() { global current_step, is_running, abort_flag global next_btn, auto_btn, skip_btn, reset_btn
if is_running return
if current_step > 5 { ExitApp() return }
if !WinExist(EAC_WIN) { RefreshUi("错误", "未找到 EAC 进程。请先启动 Exact Audio Copy。", 0) return }
is_running := true abort_flag := false
next_btn.Enabled := false auto_btn.Enabled := false skip_btn.Enabled := false reset_btn.Enabled := false
step_name := StepName(current_step) RefreshUi("正在执行第 " current_step " 步", step_name, StepStartPercent(current_step))
try { DoStep(current_step)
finished_step := current_step current_step += 1
if current_step <= 5 { RefreshUi( "第 " finished_step " 步完成", "已暂停。下一步:" StepName(current_step), StepDonePercent(finished_step) ) } else { RefreshUi( "全部步骤完成", "EAC 自动化流程已完成。", 100 ) } } catch as e { RefreshUi( "第 " current_step " 步失败或中止", e.Message, StepStartPercent(current_step) ) }
is_running := false reset_btn.Enabled := true auto_btn.Enabled := true
if current_step <= 5 { next_btn.Text := "执行下一步" next_btn.Enabled := true skip_btn.Enabled := true } else { next_btn.Text := "关闭" next_btn.Enabled := true skip_btn.Enabled := false }}
RunAutoMode() { global current_step, is_running, abort_flag global next_btn, auto_btn, skip_btn, reset_btn
if is_running return
if !WinExist(EAC_WIN) { RefreshUi("错误", "未找到 EAC 进程。请先启动 Exact Audio Copy。", 0) return }
is_running := true abort_flag := false
next_btn.Enabled := false auto_btn.Enabled := false skip_btn.Enabled := false reset_btn.Enabled := false
current_step := 1
try { RefreshUi("全自动模式:第 1 步", StepName(1), StepStartPercent(1)) AutoStep1() current_step := 2
RefreshUi("全自动模式:第 2 步", StepName(2), StepStartPercent(2)) AutoStep2() current_step := 3
RefreshUi("全自动模式:第 3 步", StepName(3), StepStartPercent(3)) AutoStep3() current_step := 4
RefreshUi("全自动模式:第 4 步", StepName(4), StepStartPercent(4)) AutoStep4()
current_step := 6 RefreshUi( "全自动模式完成", "已完成第 1 到第 4 步。`r`n第 5 步不执行任何实际操作。", 100 ) } catch as e { RefreshUi( "全自动模式失败或中止", e.Message, StepStartPercent(current_step) ) }
is_running := false reset_btn.Enabled := true auto_btn.Enabled := true
if current_step <= 5 { next_btn.Text := "执行下一步" next_btn.Enabled := true skip_btn.Enabled := true } else { next_btn.Text := "关闭" next_btn.Enabled := true skip_btn.Enabled := false }}
DoStep(step) { switch step { case 1: StepDetail("发送菜单命令:操作 > 检测间隙") SendStepMenu(1) StepDetail("已发送“检测间隙”命令。`r`n按钮将保持禁用 3 秒。") Sleep 3000
case 2: StepDetail("发送菜单命令:操作 > 检测静音间隙") SendStepMenu(2) StepDetail("已发送“检测静音间隙”命令。`r`n按钮将保持禁用 3 秒。") Sleep 3000
case 3: StepDetail("发送菜单命令:操作 > 创建 CUE 目录文件 > 多个带间隙的 WAV 文件...(非规则)") SendStepMenu(3) StepDetail("已发送“创建多个带间隙的 WAV 文件”命令。`r`n按钮将保持禁用 3 秒。") Sleep 3000
case 4: StepDetail("发送菜单命令:操作 > 测试并抓取所选音轨 > 已压缩...") SendStepMenu(4) StepDetail("已发送“测试并抓取,已压缩”命令。`r`n脚本不会等待抓取完成。`r`n按钮将保持禁用 3 秒。") Sleep 3000
case 5: StepDetail("第 5 步不执行任何实际操作。`r`n流程到此结束。") Sleep 500 }}
AutoStep1() { StepDetail("全自动第 1 步:发送“检测间隙”命令。") SendStepMenu(1)
StepDetail("全自动第 1 步:等待 EAC 的“正在分析”弹窗出现。") WaitAnalyzeDialogAppearThenDisappear("检测间隙")
StepDetail("全自动第 1 步完成:“正在分析”弹窗已消失。")}
AutoStep2() { StepDetail("全自动第 2 步:发送“检测静音间隙”命令。") SendStepMenu(2)
StepDetail("全自动第 2 步:等待标题为“正在分析”且文本包含“测试静音中的间隙”的弹窗。") WaitSilentGapDialogAndClickOk()
StepDetail("全自动第 2 步完成:已在按钮变为“确定”后点击。")}
AutoStep3() { StepDetail("全自动第 3 步:发送“创建多个带间隙的 WAV 文件(非规则)”命令。") SendStepMenu(3)
StepDetail("全自动第 3 步:等待 EAC 的“正在分析”弹窗出现。") WaitAnalyzeDialogAppearThenDisappear("创建多个带间隙的 WAV 文件,非规则")
StepDetail("全自动第 3 步完成:“正在分析”弹窗已消失。")}
AutoStep4() { StepDetail("全自动第 4 步:发送“测试并抓取所选音轨,已压缩”命令。") SendStepMenu(4)
StepDetail("全自动第 4 步:等待 EAC 抓取或压缩相关窗口出现后消失。`r`n本步骤不设置等待超时。") WaitAnyEacDialogAppearThenDisappearNoTimeout("测试并抓取,已压缩")
StepDetail("全自动第 4 步完成:EAC 相关窗口已消失。")}
SendStepMenu(step) { switch step { case 1: SelectEacMenuWithRetry(60000, ["操作(&A)", "检测间隙(&D)"], ["操作(A)", "检测间隙(D)"], ["操作", "检测间隙"] )
case 2: SelectEacMenuWithRetry(60000, ["操作(&A)", "检测静音间隙(&G)"], ["操作(A)", "检测静音间隙(G)"], ["操作", "检测静音间隙"] )
case 3: SelectEacMenuWithRetry(60000, ["操作(&A)", "创建 CUE 目录文件(&S)", "4&"], ["操作(A)", "创建 CUE 目录文件(S)", "4&"], ["操作", "创建 CUE 目录文件", "4&"], )
case 4: SelectEacMenuWithRetry(60000, ["操作(&A)", "测试并抓取所选音轨(&O)", "已压缩..."], ["操作(A)", "测试并抓取所选音轨(O)", "已压缩..."], ["操作", "测试并抓取所选音轨", "已压缩..."] )
default: throw Error("未知步骤:" step) }}
SelectEacMenuWithRetry(timeout_ms, menu_paths*) { global EAC_WIN
deadline := A_TickCount + timeout_ms last_error := ""
while A_TickCount < deadline { CheckAbort()
for path in menu_paths { try { WinActivate(EAC_WIN) WinWaitActive(EAC_WIN, , 2) MenuSelect(EAC_WIN, "", path*) Sleep 500 return true } catch as e { last_error := e.Message } }
Sleep 800 }
throw Error("菜单项执行失败。最后错误:" last_error)}
WaitAnalyzeDialogAppearThenDisappear(label) { hwnd := WaitEacDialog("正在分析", "", 60000, label) StepDetail(label ":已检测到“正在分析”弹窗。`r`n正在等待该弹窗消失。") WaitWindowGone(hwnd, 0, label)}
WaitSilentGapDialogAndClickOk() { seen_target := false last_info := ""
StepDetail("检测静音间隙:等待 EAC 的“正在分析”弹窗。`r`n不再要求先检测到“取消”。")
loop { CheckAbort()
hwnds := []
try hwnds := WinGetList("ahk_class #32770 ahk_exe EAC.exe") catch { hwnds := [] }
target_hwnd := 0 target_title := "" target_text := "" target_btn_text := ""
for hwnd in hwnds { title := "" text := "" btn_text := ""
try title := WinGetTitle("ahk_id " hwnd) catch { continue }
if !InStr(title, "正在分析") continue
try text := WinGetText("ahk_id " hwnd) catch { text := "" }
if !InStr(text, "测试静音中的间隙") continue
try btn_text := ControlGetText("Button1", "ahk_id " hwnd) catch { btn_text := "" }
target_hwnd := hwnd target_title := title target_text := text target_btn_text := btn_text break }
if target_hwnd { seen_target := true
btn_norm := NormalizeButtonText(target_btn_text)
last_info := "窗口标题:" target_title "`r`n" . "Button1 文本:" target_btn_text "`r`n" . "正在等待 Button1 变为“确定”。"
StepDetail("检测静音间隙进行中。`r`n" last_info)
if btn_norm = "确定" || btn_norm = "OK" { Sleep 500
try { ControlClick("Button1", "ahk_id " target_hwnd) } catch as e { try { WinActivate("ahk_id " target_hwnd) WinWaitActive("ahk_id " target_hwnd, , 2) Send "{Enter}" } catch as e2 { throw Error("点击 Button1 失败:" e.Message ";发送 Enter 也失败:" e2.Message) } }
WaitWindowGone(target_hwnd, 0, "检测静音间隙确认弹窗") return true } } else { if seen_target { StepDetail( "检测静音间隙:目标弹窗暂时未找到。`r`n" "可能正在刷新窗口状态,继续等待。`r`n`r`n" last_info ) } else { StepDetail("检测静音间隙:尚未检测到目标“正在分析”弹窗。") } }
Sleep 100 }}
WaitAnyEacDialogAppearThenDisappearNoTimeout(label) { hwnd := WaitAnyEacRelatedWindow(0, label)
title := "" text := ""
try title := WinGetTitle("ahk_id " hwnd) try text := WinGetText("ahk_id " hwnd)
text := CleanText(text)
if StrLen(text) > 500 text := SubStr(text, 1, 500) "..."
StepDetail( label ":已检测到 EAC 相关窗口。`r`n" "窗口标题:" title "`r`n`r`n" text "`r`n`r`n" "正在等待该窗口消失。`r`n" "本步骤不设置等待超时。" )
WaitWindowGone(hwnd, 0, label)}
WaitEacDialog(title_contains, text_contains := "", timeout_ms := 60000, label := "") { deadline := timeout_ms > 0 ? A_TickCount + timeout_ms : 0
loop { CheckAbort()
if timeout_ms > 0 && A_TickCount > deadline { if text_contains != "" throw Error("等待弹窗超时:" label "。标题包含:" title_contains ",文本包含:" text_contains) else throw Error("等待弹窗超时:" label "。标题包含:" title_contains) }
hwnd := FindEacDialog(title_contains, text_contains)
if hwnd return hwnd
Sleep 250 }}
FindEacDialog(title_contains := "", text_contains := "") { hwnds := []
try hwnds := WinGetList("ahk_class #32770 ahk_exe EAC.exe") catch { return 0 }
for hwnd in hwnds { title := "" text := ""
try title := WinGetTitle("ahk_id " hwnd) catch { continue }
if title_contains != "" && !InStr(title, title_contains) continue
if text_contains != "" { try text := WinGetText("ahk_id " hwnd) catch { continue }
if !InStr(text, text_contains) continue }
return hwnd }
return 0}
WaitAnyEacRelatedWindow(timeout_ms := 0, label := "") { deadline := timeout_ms > 0 ? A_TickCount + timeout_ms : 0
loop { CheckAbort()
if timeout_ms > 0 && A_TickCount > deadline throw Error("等待 EAC 相关弹窗超时:" label)
hwnds := []
try hwnds := WinGetList("ahk_class #32770 ahk_exe EAC.exe") catch { hwnds := [] }
for hwnd in hwnds { title := "" text := ""
try title := WinGetTitle("ahk_id " hwnd) try text := WinGetText("ahk_id " hwnd)
if title != "" || text != "" return hwnd }
Sleep 500 }}
WaitWindowGone(hwnd, timeout_ms := 0, label := "") { deadline := timeout_ms > 0 ? A_TickCount + timeout_ms : 0
loop { CheckAbort()
if !WinExist("ahk_id " hwnd) return true
if timeout_ms > 0 && A_TickCount > deadline throw Error("等待窗口消失超时:" label)
Sleep 250 }}
NormalizeButtonText(s) { s := StrReplace(s, "&", "") s := RegExReplace(s, "\([A-Za-z]\)", "") s := RegExReplace(s, "([A-Za-z])", "") return Trim(s)}
CleanText(s) { s := StrReplace(s, "`r", "") s := RegExReplace(s, "`n{3,}", "`n`n") return Trim(s)}
SkipCurrentStep() { global current_step, is_running global next_btn, auto_btn, skip_btn, reset_btn
if is_running return
if current_step > 5 { ExitApp() return }
skipped_step := current_step skipped_name := StepName(skipped_step)
current_step += 1
if current_step <= 5 { RefreshUi( "已跳过第 " skipped_step " 步", "未执行:" skipped_name "`r`n下一步:" StepName(current_step), StepDonePercent(skipped_step) )
next_btn.Text := "执行下一步" next_btn.Enabled := true auto_btn.Enabled := true skip_btn.Enabled := true reset_btn.Enabled := true } else { RefreshUi( "全部步骤完成", "已跳过第 " skipped_step " 步:" skipped_name "`r`nEAC 自动化流程已完成。", 100 )
next_btn.Text := "关闭" next_btn.Enabled := true auto_btn.Enabled := true skip_btn.Enabled := false reset_btn.Enabled := true }}
ResetFlow() { global current_step, is_running global next_btn, auto_btn, skip_btn, reset_btn
if is_running return
current_step := 1
next_btn.Text := "执行下一步" next_btn.Enabled := true auto_btn.Enabled := true skip_btn.Enabled := true reset_btn.Enabled := true
RefreshUi("已重置", "请点击“执行下一步”,或点击“全自动模式”。", 0)}
CheckAbort() { global abort_flag
if abort_flag throw Error("用户通过 UI 中止。")}
RefreshUi(status, detail, percent) { global current_step global title_text, status_text, next_text, detail_text, progress_bar, next_btn
title_text.Text := "EAC 自动化控制" status_text.Text := "状态:" status detail_text.Value := detail progress_bar.Value := percent
if current_step <= 5 { next_text.Text := "下一步:" current_step "/5," StepName(current_step) next_btn.Text := "执行下一步" } else { next_text.Text := "下一步:无,流程已完成" next_btn.Text := "关闭" }}
StepDetail(text) { global detail_text
if IsObject(detail_text) detail_text.Value := text}
StepName(step) { switch step { case 1: return "检测间隙" case 2: return "检测静音间隙" case 3: return "创建多个带间隙的 WAV 文件,非规则" case 4: return "测试并抓取,已压缩" case 5: return "抓取完成后确定" default: return "完成" }}
StepStartPercent(step) { switch step { case 1: return 0 case 2: return 20 case 3: return 40 case 4: return 60 case 5: return 80 default: return 100 }}
StepDonePercent(step) { switch step { case 1: return 20 case 2: return 40 case 3: return 60 case 4: return 80 case 5: return 100 default: return 100 }} 自动化EAC音频抓取
https://nptr.cc/posts/2026-05/auto-eac/