—————【下 載 地 址】———————
【?本章下載一】:https://pan.xunlei.com/s/VOVvLpQuRxYadClkxTGwO2OnA1?pwd=vind#
【?本章下載二】:https://pan.xunlei.com/s/VOVvLpQuRxYadClkxTGwO2OnA1?pwd=vind#
【百款黑科技】:https://ucnygalh6wle.feishu.cn/wiki/HPQywvPc7iLZu1k0ODFcWMt2n0d?from=from_copylink
—————【下 載 地 址】———————
寫在前面?最近在電腦上跟人聊天時發現它不能像手機那樣自動錄音,找了一圈也沒發現類似的軟件。于是作為小白的我,在 AI 的幫助下完成了這個工具。如果你也有類似需求,希望這款微信通話自動錄音器能幫到你。
軟件簡介
微信通話自動錄音器 是一款支持 Windows 平臺的桌面小工具,主要功能包括:
自動檢測微信的通話窗口(支持語音 / 視頻通話)
一旦檢測到通話,自動開始錄音,通話結束自動保存 mp3 文件
支持選擇麥克風或虛擬聲卡(推薦安裝 VB-Audio Virtual Cable,可完整捕獲 "你和對方" 的聲音)
支持自定義錄音保存路徑
使用步驟
下載并解壓本程序(建議放在英文路徑下)
安裝 FFmpeg(用于錄音處理,詳見下文)
安裝虛擬聲卡(推薦 VB-Audio Virtual Cable)
設置系統“偵聽”功能(詳見下文)
雙擊運行程序,選擇輸入設備與保存路徑
最小化后可在系統托盤中運行,程序將自動錄音
虛擬聲卡安裝方法
打開官網:[url=]https://vb-audio.com/Cable/[/url]
點擊 Download 下載壓縮包(如 VBCABLE_Driver_Pack43.zip)
解壓后,右鍵以管理員身份運行 VBCABLE_Setup_x64.exe
點擊 Install Driver 并按提示完成安裝,重啟電腦后生效
系統“偵聽”功能設置(非常重要)
右鍵任務欄右下角喇叭圖標 → 選擇“聲音設置”
點擊右側“更多聲音設置” → 切換到“錄制”標簽頁
找到要錄音的設備(例如:“麥克風”、“CABLE Output” 等),右鍵 → 選擇“屬性”
切換到“偵聽”標簽頁 → 勾選“偵聽此設備”
播放設備選擇你常用的揚聲器或耳機(建議不要選虛擬聲卡)
點擊“應用” → “確定”- [Python]?純文本查看?復制代碼?001002003004005006007008009010011012013014015016017018019020021022023024025026027028029030031032033034035036037038039040041042043044045046047048049050051052053054055056057058059060061062063064065066067068069070071072073074075076077078079080081082083084085086087088089090091092093094095096097098099100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
import
?os
import
?sys
import
?json
import
?subprocess
import
?threading
import
?time
import
?tkinter as tk
from
?tkinter?
import
?ttk, filedialog, messagebox
import
?pygetwindow as gw
import
?pystray
from
?PIL?
import
?Image, ImageDraw
import
?logging
import
?traceback
from
?logging.handlers?
import
?TimedRotatingFileHandler
import
?ctypes
import
?webbrowser
import
?glob
import
?tempfile
import
?shutil
import
?stat
?def
?check_single_instance():
????
mutex?
=
?ctypes.windll.kernel32.CreateMutexW(
None
,?
1
,?
"WechatRecorderMutex-ByNightingale"
)
????
last_error?
=
?ctypes.windll.kernel32.GetLastError()
????
if
?last_error?
=
=
?183
:
????????
root?
=
?tk.Tk()
????????
root.withdraw()
????????
messagebox.showerror(
"已在運行"
,?
"微信通話自動錄音器已經在運行,無法多開。"
)
????????
sys.exit(
0
)
?check_single_instance()
?def
?resource_path(relative_path):
????
if
?hasattr
(sys,?
'_MEIPASS'
):
????????
return
?os.path.join(sys._MEIPASS, relative_path)
????
return
?os.path.join(os.path.dirname(__file__), relative_path)
?LOG_DIR?
=
?'log'
os.makedirs(LOG_DIR, exist_ok
=
True
)
log_file?
=
?os.path.join(LOG_DIR,?
'wechat_recorder.log'
)
handler?
=
?TimedRotatingFileHandler(
????
log_file,
????
when
=
'midnight'
,
????
interval
=
1
,
????
backupCount
=
30
,
????
encoding
=
'utf-8'
)
handler.suffix?
=
?"%Y-%m-%d.log"
logging.basicConfig(
????
handlers
=
[handler],
????
level
=
logging.DEBUG,
????
format
=
'%(asctime)s - %(levelname)s - %(threadName)s - %(module)s.%(funcName)s:%(lineno)d - %(message)s'
)
?CONFIG_FILE?
=
?'config.json'
RECORDINGS_DIR?
=
?'recordings'
CALL_WINDOW_CLASSES?
=
?{
"ILinkAudioWnd"
,?
"AudioWnd"
,?
"ILinkVoipTrayWnd"
}
?def
?get_window_class(hwnd):
????
buff?
=
?ctypes.create_unicode_buffer(
256
)
????
ctypes.windll.user32.GetClassNameW(hwnd, buff,?
256
)
????
return
?buff.value
?def
?clean_test_recordings(save_path):
????
pattern?
=
?os.path.join(save_path,?
"測試錄音_*.mp3"
)
????
for
?f?
in
?glob.glob(pattern):
????????
try
:
????????????
os.remove(f)
????????????
logging.info(f
'清理殘留測試錄音文件:{f}'
)
????????
except
?Exception:
????????????
logging.warning(f
'無法清理測試錄音文件(被占用?):{f}'
)
?class
?WeChatRecorder:
????
def
?__init__(
self
):
????????
logging.debug(
'初始化 WeChatRecorder 實例'
)
????????
self
.load_config()
????????
os.makedirs(
self
.config.get(
'save_path'
, os.path.abspath(RECORDINGS_DIR)), exist_ok
=
True
)
????????
clean_test_recordings(
self
.config.get(
'save_path'
, os.path.abspath(RECORDINGS_DIR)))
????????
self
.recording?
=
?False
????????
self
.recording_thread?
=
?None
????????
self
.input_device_map?
=
?{}
????????
self
.check_thread?
=
?threading.Thread(target
=
self
.monitor_wechat_window, daemon
=
True
, name
=
'MonitorThread'
)
????????
self
.input_devices?
=
?[]
????????
self
.ffmpeg_path?
=
?self
.find_ffmpeg()
????????
if
?not
?self
.ffmpeg_path:
????????????
messagebox.showerror(
????????????????
"無法找到FFmpeg"
,
????????????????
"未檢測到 ffmpeg.exe,請將其放入程序目錄下的 ffmpeg_bin 文件夾,或安裝到系統環境變量中。"
????????????
)
????????????
sys.exit(
1
)
????????
self
.setup_ui()
????????
self
.setup_tray_icon()
????????
logging.info(
'程序啟動完成,UI 和托盤初始化完成'
)
?????
def
?find_ffmpeg(
self
):
????????
try
:
????????????
ffmpeg_path?
=
?os.path.join(os.path.dirname(sys.executable),?
'ffmpeg.exe'
)
????????????
if
?os.path.exists(ffmpeg_path):
????????????????
logging.info(f
"已找到同級 ffmpeg.exe: {ffmpeg_path}"
)
????????????????
return
?ffmpeg_path
?????????????
# fallback:嘗試系統環境變量
????????????
subprocess.run([
"ffmpeg"
,?
"-version"
], stdout
=
subprocess.PIPE, stderr
=
subprocess.PIPE, timeout
=
2
)
????????????
logging.info(
"使用系統環境變量中的 ffmpeg"
)
????????????
return
?"ffmpeg"
?????????
except
?Exception as e:
????????????
logging.error(f
"FFmpeg 檢測失敗: {e}"
)
????????????
return
?None
?????
def
?load_config(
self
):
????????
try
:
????????????
if
?os.path.exists(CONFIG_FILE):
????????????????
with?
open
(CONFIG_FILE,?
'r'
, encoding
=
'utf-8'
) as f:
????????????????????
self
.config?
=
?json.load(f)
????????????????
logging.debug(f
'讀取配置文件 {CONFIG_FILE}: {self.config}'
)
????????????
else
:
????????????????
self
.config?
=
?{
????????????????????
'input_device'
:?
'default'
,
????????????????????
'save_path'
: os.path.abspath(RECORDINGS_DIR),
????????????????????
'on_close'
:?
'minimize'
????????????????
}
????????????????
logging.debug(f
'未找到配置文件,使用默認配置: {self.config}'
)
????????
except
?Exception as e:
????????????
logging.error(f
'加載配置失敗: {e}\n{traceback.format_exc()}'
)
????????????
self
.config?
=
?{
????????????????
'input_device'
:?
'default'
,
????????????????
'save_path'
: os.path.abspath(RECORDINGS_DIR),
????????????????
'on_close'
:?
'minimize'
????????????
}
?????
def
?save_config(
self
):
????????
try
:
????????????
with?
open
(CONFIG_FILE,?
'w'
, encoding
=
'utf-8'
) as f:
????????????????
json.dump(
self
.config, f, indent
=
2
, ensure_ascii
=
False
)
????????????
logging.info(f
'配置已保存到 {CONFIG_FILE}: {self.config}'
)
????????
except
?Exception as e:
????????????
logging.error(f
'保存配置失敗: {e}\n{traceback.format_exc()}'
)
?????
def
?get_audio_devices(
self
):
????????
self
.input_device_map?
=
?{}
????????
input_display_names?
=
?[]
????????
try
:
????????????
import
?sounddevice as sd
????????????
devices?
=
?sd.query_devices()
????????????
default_input?
=
?sd.default.device[
0
]
????????????
if
?default_input >
=
?0
:
????????????????
default_prefix?
=
?devices[default_input][
'name'
].split(
'('
)[
0
].strip()
????????????????
longest?
=
?''
????????????????
for
?dev?
in
?devices:
????????????????????
name?
=
?dev[
'name'
]
????????????????????
if
?dev[
'max_input_channels'
] >?
0
:
????????????????????????
prefix?
=
?name.split(
'('
)[
0
].strip()
????????????????????????
if
?prefix?
=
=
?default_prefix?
and
?len
(name) >?
len
(longest):
????????????????????????????
longest?
=
?name
????????????????
if
?not
?longest:
????????????????????
longest?
=
?devices[default_input][
'name'
]
????????????????
self
.input_device_map[
'default'
]?
=
?longest
????????????????
input_display_names.append(
'default (系統默認設備)'
)
????????????????
logging.info(f
'系統默認輸入設備用全稱: {longest}'
)
????????????
name_prefix_map?
=
?{}
????????????
for
?dev?
in
?devices:
????????????????
name?
=
?dev[
'name'
]
????????????????
if
?dev[
'max_input_channels'
] >?
0
:
????????????????????
prefix?
=
?name.split(
'('
)[
0
].strip()
????????????????????
if
?(prefix?
not
?in
?name_prefix_map)?
or
?(
len
(name) >?
len
(name_prefix_map[prefix])):
????????????????????????
name_prefix_map[prefix]?
=
?name
????????????
for
?name?
in
?sorted
(name_prefix_map.values()):
????????????????
self
.input_device_map[name]?
=
?name
????????????????
input_display_names.append(name)
????????????????
logging.info(f
'最終輸入設備: {name}'
)
????????
except
?Exception as e:
????????????
logging.error(f
'設備檢測錯誤: {e}\n{traceback.format_exc()}'
)
????????????
input_display_names?
=
?[
"default (系統默認設備)"
]
????????
logging.info(f
'最終輸入設備列表: {input_display_names}'
)
????????
return
?input_display_names
?????
def
?detect_virtual_cable(
self
):
????????
for
?name?
in
?self
.input_devices:
????????????
if
?"VB-Audio Virtual Cable"
?in
?name?
or
?"CABLE Output"
?in
?name?
or
?"CABLE Input"
?in
?name:
????????????????
return
?True
????????
return
?False
?????
def
?setup_ui(
self
):
????????
logging.debug(
'初始化UI界面'
)
????????
self
.root?
=
?tk.Tk()
????????
ico_path?
=
?resource_path(
'wechat_recorder.ico'
)
????????
try
:
????????????
self
.root.iconbitmap(ico_path)
????????
except
?Exception as e:
????????????
logging.warning(f
'icon設置失敗: {e}'
)
????????
self
.root.title(
'微信通話自動錄音器--by夜鶯'
)
????????
self
.root.geometry(
'480x380'
)
????????
self
.root.protocol(
"WM_DELETE_WINDOW"
,?
self
.on_close)
?????????
frame?
=
?ttk.Frame(
self
.root, padding
=
10
)
????????
frame.pack(fill
=
tk.BOTH, expand
=
True
)
?????????
self
.input_devices?
=
?self
.get_audio_devices()
????????
has_virtual_cable?
=
?self
.detect_virtual_cable()
????????
if
?not
?has_virtual_cable:
????????????
top_notice?
=
?(
????????????????
"⚠️ 未檢測到虛擬聲卡,建議安裝 [VB-Audio Virtual Cable] 以獲得完整錄音效果。\n"
????????????????
"請點擊下方按鈕打開官網下載頁面,下載后手動安裝(需管理員權限),安裝成功后請重啟本軟件。"
????????????
)
????????????
lbl?
=
?ttk.Label(frame, text
=
top_notice, foreground
=
"red"
, wraplength
=
450
, justify
=
"left"
)
????????????
lbl.pack(fill
=
tk.X, pady
=
(
0
,?
5
))
????????????
ttk.Button(frame, text
=
'打開VB-Audio官方主頁'
,
???????????????????????
command
=
lambda
: webbrowser.
open
(
"https://vb-audio.com/Cable/"
)).pack(pady
=
2
)
?????????
ttk.Label(frame, text
=
'選擇錄音輸入設備:'
).pack(anchor
=
'w'
)
????????
self
.input_device_combo?
=
?ttk.Combobox(frame, values
=
self
.input_devices, state
=
'readonly'
)
????????
self
.input_device_combo.pack(fill
=
tk.X)
????????
current_input?
=
?self
.config.get(
'input_device'
,?
'default'
)
????????
if
?current_input?
=
=
?'default'
?and
?'default (系統默認設備)'
?in
?self
.input_devices:
????????????
self
.input_device_combo.
set
(
'default (系統默認設備)'
)
????????
elif
?current_input?
in
?self
.input_device_map?
and
?current_input?
in
?self
.input_devices:
????????????
self
.input_device_combo.
set
(current_input)
????????
elif
?self
.input_devices:
????????????
self
.input_device_combo.
set
(
self
.input_devices[
0
])
????????
logging.debug(f
'當前選擇的輸入設備: {self.input_device_combo.get()}'
)
?????????
volume_frame?
=
?ttk.Frame(frame)
????????
volume_frame.pack(fill
=
tk.X, pady
=
(
6
,?
0
))
????????
ttk.Label(volume_frame, text
=
'實時音量:'
).pack(side
=
tk.LEFT)
????????
self
.volume_progressbar?
=
?ttk.Progressbar(volume_frame, orient
=
"horizontal"
, length
=
180
, mode
=
"determinate"
, maximum
=
100
)
????????
self
.volume_progressbar.pack(side
=
tk.LEFT, padx
=
5
)
????????
self
.volume_label?
=
?ttk.Label(volume_frame, text
=
'0%'
)
????????
self
.volume_label.pack(side
=
tk.LEFT, padx
=
5
)
????????
self
.test_record_btn?
=
?ttk.Button(volume_frame, text
=
'播放錄音'
, command
=
self
.test_record_and_play)
????????
self
.test_record_btn.pack(side
=
tk.LEFT, padx
=
8
)
?????????
self
._monitor_volume?
=
?True
????????
self
._recording_in_progress?
=
?False
????????
self
.root.after(
500
,?
self
.update_volume_bar)
?????????
ttk.Label(frame, text
=
'錄音保存路徑:'
).pack(anchor
=
'w'
, pady
=
(
10
,?
0
))
????????
self
.path_entry?
=
?ttk.Entry(frame)
????????
self
.path_entry.insert(
0
,?
self
.config[
'save_path'
])
????????
self
.path_entry.pack(fill
=
tk.X)
????????
ttk.Button(frame, text
=
'選擇路徑...'
, command
=
self
.select_path).pack(pady
=
5
)
?????????
self
.minimize_var?
=
?tk.StringVar(value
=
self
.config.get(
'on_close'
,?
'minimize'
))
????????
ttk.Radiobutton(frame, text
=
'最小化到托盤'
, variable
=
self
.minimize_var, value
=
'minimize'
).pack(anchor
=
'w'
)
????????
ttk.Radiobutton(frame, text
=
'直接退出'
, variable
=
self
.minimize_var, value
=
'exit'
).pack(anchor
=
'w'
)
????????
ttk.Button(frame, text
=
'保存設置'
, command
=
self
.save_ui_config).pack(pady
=
10
)
?????
def
?update_volume_bar(
self
):
????????
try
:
????????????
import
?sounddevice as sd
????????????
import
?numpy as np
?????????????
if
?getattr
(
self
,?
'_recording_in_progress'
,?
False
):
????????????????
self
.volume_progressbar[
'value'
]?
=
?0
????????????????
self
.volume_label[
'foreground'
]?
=
?'gray'
????????????????
self
.volume_label[
'text'
]?
=
?'錄音中'
????????????
else
:
????????????????
selected_name?
=
?self
.input_device_combo.get()
????????????????
devices?
=
?sd.query_devices()
????????????????
matched_index?
=
?None
?????????????????
for
?idx, dev?
in
?enumerate
(devices):
????????????????????
if
?selected_name?
in
?dev[
'name'
]?
and
?dev[
'max_input_channels'
] >?
0
:
????????????????????????
matched_index?
=
?idx
????????????????????????
break
?????????????????
# 如果是“default (系統默認設備)”或找不到,就用默認設備
????????????????
if
?selected_name?
=
=
?'default (系統默認設備)'
?or
?matched_index?
is
?None
:
????????????????????
matched_index?
=
?None
?????????????????
fs?
=
?16000
????????????????
duration?
=
?0.07
????????????????
data?
=
?sd.rec(
int
(duration?
*
?fs), samplerate
=
fs, channels
=
1
, device
=
matched_index, blocking
=
True
)
????????????????
if
?data?
is
?not
?None
?and
?data.
any
():
????????????????????
rms?
=
?float
(np.sqrt(np.mean(np.square(data))))
????????????????????
percent?
=
?min
(
int
(rms?
*
?4000
),?
100
)
????????????????????
self
.volume_progressbar[
'value'
]?
=
?percent
????????????????????
self
.volume_label[
'foreground'
]?
=
?'black'
????????????????????
self
.volume_label[
'text'
]?
=
?f
'{percent}%'
????????????????
else
:
????????????????????
self
.volume_progressbar[
'value'
]?
=
?0
????????????????????
self
.volume_label[
'text'
]?
=
?'0%'
????????
except
?Exception as e:
????????????
import
?traceback
????????????
logging.warning(f
'音量獲取失敗: {e}\n{traceback.format_exc()}'
)
????????????
self
.volume_progressbar[
'value'
]?
=
?0
????????????
self
.volume_label[
'foreground'
]?
=
?'black'
????????????
self
.volume_label[
'text'
]?
=
?'0%'
????????
finally
:
????????????
if
?getattr
(
self
,?
'_monitor_volume'
,?
False
):
????????????????
self
.root.after(
200
,?
self
.update_volume_bar)
?????
def
?test_record_and_play(
self
):
????????
from
?datetime?
import
?datetime
?????????
self
.test_record_btn[
'state'
]?
=
?tk.DISABLED
????????
self
.volume_label[
'text'
]?
=
?'測試中'
????????
self
._recording_in_progress?
=
?True
????????
self
.root.update_idletasks()
?????????
timestamp?
=
?datetime.now().strftime(
'%Y-%m-%d_%H-%M-%S'
)
????????
filename?
=
?os.path.join(
????????????
self
.config.get(
'save_path'
, os.path.abspath(RECORDINGS_DIR)),
????????????
f
'測試錄音_{timestamp}.mp3'
????????
)
????????
selected_device?
=
?self
.input_device_combo.get()
????????
if
?selected_device?
=
=
?'default (系統默認設備)'
:
????????????
input_name?
=
?self
.input_device_map[
'default'
]
????????
else
:
????????????
input_name?
=
?selected_device
?????????
cmd?
=
?[
????????????
self
.ffmpeg_path,
????????????
'-f'
,?
'dshow'
,?
'-i'
, f
'audio={input_name}'
,
????????????
'-t'
,?
'5'
,
????????????
'-acodec'
,?
'libmp3lame'
,
????????????
'-y'
, filename
????????
]
????????
logging.info(f
"測試錄音命令: {' '.join(cmd)}"
)
????????
def
?record_and_play():
????????????
try
:
????????????????
p?
=
?subprocess.Popen(
????????????????????
cmd,
????????????????????
stdout
=
subprocess.PIPE,
????????????????????
stderr
=
subprocess.PIPE,
????????????????????
creationflags
=
getattr
(subprocess,?
'CREATE_NO_WINDOW'
,?
0
)
????????????????
)
????????????????
p.wait()
????????????????
if
?os.path.exists(filename):
????????????????????
os.startfile(filename)
????????????????
else
:
????????????????????
messagebox.showerror(
"測試錄音失敗"
,?
"未生成錄音文件,請檢查設備和設置。"
)
????????????
except
?Exception as e:
????????????????
messagebox.showerror(
"測試錄音失敗"
, f
"錄音失敗:{e}"
)
????????????
finally
:
????????????????
time.sleep(
1
)
????????????????
self
.test_record_btn[
'state'
]?
=
?tk.NORMAL
????????????????
self
._recording_in_progress?
=
?False
????????????????
self
.volume_label[
'text'
]?
=
?'0%'
?????????
threading.Thread(target
=
record_and_play, daemon
=
True
).start()
?????
def
?setup_tray_icon(
self
):
????????
logging.debug(
'初始化托盤圖標'
)
????????
try
:
????????????
icon_path?
=
?resource_path(
'wechat_recorder.ico'
)
????????????
image?
=
?Image.
open
(icon_path)
????????
except
?Exception:
????????????
image?
=
?Image.new(
'RGB'
, (
64
,?
64
), color
=
'white'
)
????????????
draw?
=
?ImageDraw.Draw(image)
????????????
draw.rectangle((
8
,?
20
,?
56
,?
44
), fill
=
'black'
)
????????
self
.icon?
=
?pystray.Icon(
????????????
"wechat_recorder"
,
????????????
image,
????????????
"微信自動錄音器"
,
????????????
menu
=
pystray.Menu(
????????????????
pystray.MenuItem(
'打開設置'
,?
self
.show_window),
????????????????
pystray.MenuItem(
'退出'
,?
self
.quit_app)
????????????
)
????????
)
?????
def
?select_path(
self
):
????????
path?
=
?filedialog.askdirectory()
????????
if
?path:
????????????
logging.info(f
'用戶選擇保存路徑: {path}'
)
????????????
self
.path_entry.delete(
0
, tk.END)
????????????
self
.path_entry.insert(
0
, path)
?????
def
?save_ui_config(
self
):
????????
selected_input?
=
?self
.input_device_combo.get()
????????
self
.config[
'input_device'
]?
=
?'default'
?if
?selected_input?
=
=
?'default (系統默認設備)'
?else
?selected_input
????????
self
.config[
'save_path'
]?
=
?self
.path_entry.get()
????????
self
.config[
'on_close'
]?
=
?self
.minimize_var.get()
????????
self
.save_config()
????????
messagebox.showinfo(
"提示"
,?
"設置已保存"
)
????????
logging.info(f
"保存配置: 輸入設備={self.config['input_device']} 路徑={self.config['save_path']}"
)
?????
def
?on_close(
self
):
????????
if
?self
.minimize_var.get()?
=
=
?'minimize'
:
????????????
self
.root.withdraw()
????????????
if
?not
?self
.icon.visible:
????????????????
threading.Thread(target
=
self
.icon.run, name
=
'TrayThread'
, daemon
=
True
).start()
????????????
logging.info(
"窗口最小化到托盤"
)
????????
else
:
????????????
self
.quit_app()
?????
def
?show_window(
self
,?
*
args):
????????
logging.info(
'顯示主窗口'
)
????????
try
:
????????????
self
.root.deiconify()
????????
except
?Exception as e:
????????????
logging.warning(f
'主窗口顯示異常: {e}'
)
?????
def
?quit_app(
self
,?
*
args):
????????
logging.info(
'接收到退出請求,準備退出'
)
????????
try
:
????????????
if
?hasattr
(
self
,?
"icon"
)?
and
?self
.icon.visible:
????????????????
self
.icon.stop()
????????
except
?Exception as e:
????????????
logging.warning(f
'托盤退出異常: {e}'
)
????????
try
:
????????????
self
.root.destroy()
????????
except
?Exception as e:
????????????
logging.warning(f
'窗口銷毀異常: {e}'
)
????????
logging.info(
"程序退出"
)
????????
os._exit(
0
)
?????
def
?monitor_wechat_window(
self
):
????????
logging.debug(
'啟動微信窗口監控線程'
)
????????
while
?True
:
????????????
try
:
????????????????
all_windows?
=
?gw.getAllWindows()
????????????????
in_call?
=
?False
????????????????
for
?w?
in
?all_windows:
????????????????????
try
:
????????????????????????
cls
?=
?get_window_class(w._hWnd)
????????????????????????
if
?cls
?in
?CALL_WINDOW_CLASSES:
????????????????????????????
in_call?
=
?True
????????????????????????????
break
????????????????????
except
?Exception:
????????????????????????
continue
????????????????
logging.info(f
"檢測窗口: {'在通話' if in_call else '未通話'},錄音狀態: {self.recording}"
)
????????????????
if
?in_call?
and
?not
?self
.recording:
????????????????????
logging.debug(
'檢測到通話窗口出現,準備開始錄音'
)
????????????????????
self
.start_recording()
????????????????
elif
?not
?in_call?
and
?self
.recording:
????????????????????
logging.debug(
'檢測到通話窗口關閉,準備停止錄音'
)
????????????????????
self
.stop_recording()
????????????????
time.sleep(
1
)
????????????
except
?Exception as e:
????????????????
logging.error(f
"監控微信窗口時出錯: {e}\n{traceback.format_exc()}"
)
????????????????
time.sleep(
5
)
?????
def
?start_recording(
self
):
????????
try
:
????????????
timestamp?
=
?time.strftime(
'%Y-%m-%d_%H-%M-%S'
)
????????????
filename?
=
?os.path.join(
self
.config[
'save_path'
], f
'wechat_call_{timestamp}.mp3'
)
????????????
self
.last_record_file?
=
?filename
????????????
selected_device?
=
?self
.input_device_combo.get()
????????????
if
?selected_device?
=
=
?'default (系統默認設備)'
:
????????????????
input_name?
=
?self
.input_device_map[
'default'
]
????????????
else
:
????????????????
input_name?
=
?selected_device
????????????
cmd?
=
?[
????????????????
self
.ffmpeg_path,
????????????????
'-f'
,?
'dshow'
,?
'-i'
, f
'audio={input_name}'
,
????????????????
'-acodec'
,?
'libmp3lame'
,
????????????????
'-y'
, filename
????????????
]
????????????
logging.info(f
"本次錄音命令為: {' '.join(cmd)}"
)
????????????
self
.recording?
=
?True
????????????
self
._recording_in_progress?
=
?True
????????????
self
.volume_label[
'foreground'
]?
=
?'gray'
????????????
self
.volume_label[
'text'
]?
=
?'錄音中'
????????????
self
.volume_progressbar[
'value'
]?
=
?0
????????????
self
.recording_thread?
=
?subprocess.Popen(
????????????????
cmd,
????????????????
stdout
=
subprocess.PIPE,
????????????????
stderr
=
subprocess.PIPE,
????????????????
stdin
=
subprocess.PIPE,
????????????????
creationflags
=
getattr
(subprocess,?
'CREATE_NO_WINDOW'
,?
0
)
????????????
)
????????????
logging.info(f
"錄音進程已啟動: {filename}"
)
????????
except
?Exception as e:
????????????
logging.error(f
"開始錄音失敗: {e}\n{traceback.format_exc()}"
)
????????????
self
.recording?
=
?False
????????????
self
._recording_in_progress?
=
?False
?????
def
?stop_recording(
self
):
????????
if
?self
.recording_thread:
????????????
try
:
????????????????
if
?self
.recording_thread.stdin:
????????????????????
try
:
????????????????????????
self
.recording_thread.stdin.write(b
'q\n'
)
????????????????????????
self
.recording_thread.stdin.flush()
????????????????????
except
?Exception as e:
????????????????????????
logging.warning(f
'向ffmpeg發送q命令異常: {e}'
)
????????????????
self
.recording_thread.wait(timeout
=
5
)
????????????????
logging.info(
'錄音進程已成功終止'
)
????????????????
try
:
????????????????????
stdout, stderr?
=
?self
.recording_thread.communicate(timeout
=
2
)
????????????????????
if
?stdout:
????????????????????????
logging.info(f
'ffmpeg stdout: {stdout.decode("utf-8", "ignore")}'
)
????????????????????
if
?stderr:
????????????????????????
logging.info(f
'ffmpeg stderr: {stderr.decode("utf-8", "ignore")}'
)
????????????????
except
?Exception as e:
????????????????????
logging.warning(f
'獲取ffmpeg輸出異常: {e}'
)
????????????
except
?Exception as e:
????????????????
logging.warning(f
"終止錄音進程失敗: {e}\n{traceback.format_exc()}"
)
????????????
finally
:
????????????????
self
.recording_thread?
=
?None
????????
if
?hasattr
(
self
,?
'last_record_file'
):
????????????
if
?os.path.exists(
self
.last_record_file):
????????????????
logging.info(f
"錄音文件已生成: {self.last_record_file}"
)
????????????
else
:
????????????????
logging.error(f
"錄音進程結束但未發現錄音文件: {self.last_record_file}"
)
????????
self
.recording?
=
?False
????????
self
._recording_in_progress?
=
?False
????????
self
.volume_label[
'foreground'
]?
=
?'black'
????????
self
.update_volume_bar()
????????
logging.info(
"錄音結束"
)
?????
def
?run(
self
):
????????
logging.info(
'啟動主線程,進入主循環'
)
????????
self
.check_thread.start()
????????
self
.root.mainloop()
?if
?__name__?
=
=
?'__main__'
:
????
app?
=
?WeChatRecorder()
????
app.run()
最后
這個工具比較小眾,但希望它能幫到你。如果你也有自己的“小需求”,不妨動手試試。哪怕不全懂,有 AI 幫助,一切都變得簡單了起來。
本工具純屬個人學習作品,尚未支持多線程錄音、靜音檢測等高級功能,請酌情使用。如遇問題,歡迎理性反饋或共同改進。