3097 字
15 分钟

自动化EAC音频抓取

EAC(Exact Audio Copy)广泛应用于CD抓轨中,EAC日志也已成为验证完美抓轨中不可缺少的一部分;且与XLD一样基本上成为精确抓轨的事实标准。但是EAC的配置和使用非常复杂,即使你了解所有步骤也有可能漏做几步,导致log达不到100分。本文不会讨论怎么正确的配置光驱和EAC相关的设置,这些设置是一次性的,可以看看这篇文章。当然,EAC版本需要升级到最新版,我没有在旧版EAC上测试过。

一个完整的抓轨流程需要以下步骤:

  1. 碟片进仓
  2. 检测间隙
  3. 检测静音间隙
  4. 生成非规则CUE
  5. 抓取已压缩文件

这四部所有的按钮都藏在菜单中,生成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 2
DetectHiddenWindows false
global EAC_WIN := "ahk_exe EAC.exe"
global current_step := 1
global is_running := false
global abort_flag := false
global ui := 0
global title_text := 0
global status_text := 0
global next_text := 0
global detail_text := 0
global progress_bar := 0
global next_btn := 0
global auto_btn := 0
global skip_btn := 0
global reset_btn := 0
global 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/
作者
Nullpinter
发布于
2026-05-06
许可协议
All Right Reserved.