—————【下 载 地 址】———————
【本章下载一】: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
importosimportsysimportjsonimportsubprocessimportthreadingimporttimeimporttkinter as tkfromtkinterimportttk, filedialog, messageboximportpygetwindow as gwimportpystrayfromPILimportImage, ImageDrawimportloggingimporttracebackfromlogging.handlersimportTimedRotatingFileHandlerimportctypesimportwebbrowserimportglobimporttempfileimportshutilimportstatdefcheck_single_instance():mutex=ctypes.windll.kernel32.CreateMutexW(None,1,"WechatRecorderMutex-ByNightingale")last_error=ctypes.windll.kernel32.GetLastError()iflast_error==183:root=tk.Tk()root.withdraw()messagebox.showerror("已在运行","微信通话自动录音器已经在运行,无法多开。")sys.exit(0)check_single_instance()defresource_path(relative_path):ifhasattr(sys,'_MEIPASS'):returnos.path.join(sys._MEIPASS, relative_path)returnos.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"}defget_window_class(hwnd):buff=ctypes.create_unicode_buffer(256)ctypes.windll.user32.GetClassNameW(hwnd, buff,256)returnbuff.valuedefclean_test_recordings(save_path):pattern=os.path.join(save_path,"测试录音_*.mp3")forfinglob.glob(pattern):try:os.remove(f)logging.info(f'清理残留测试录音文件:{f}')exceptException:logging.warning(f'无法清理测试录音文件(被占用?):{f}')classWeChatRecorder: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=Falseself.recording_thread=Noneself.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()ifnotself.ffmpeg_path:messagebox.showerror("无法找到FFmpeg","未检测到 ffmpeg.exe,请将其放入程序目录下的 ffmpeg_bin 文件夹,或安装到系统环境变量中。")sys.exit(1)self.setup_ui()self.setup_tray_icon()logging.info('程序启动完成,UI 和托盘初始化完成')deffind_ffmpeg(self):try:ffmpeg_path=os.path.join(os.path.dirname(sys.executable),'ffmpeg.exe')ifos.path.exists(ffmpeg_path):logging.info(f"已找到同级 ffmpeg.exe: {ffmpeg_path}")returnffmpeg_path# fallback:尝试系统环境变量subprocess.run(["ffmpeg","-version"], stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=2)logging.info("使用系统环境变量中的 ffmpeg")return"ffmpeg"exceptException as e:logging.error(f"FFmpeg 检测失败: {e}")returnNonedefload_config(self):try:ifos.path.exists(CONFIG_FILE):withopen(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}')exceptException 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'}defsave_config(self):try:withopen(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}')exceptException as e:logging.error(f'保存配置失败: {e}\n{traceback.format_exc()}')defget_audio_devices(self):self.input_device_map={}input_display_names=[]try:importsounddevice as sddevices=sd.query_devices()default_input=sd.default.device[0]ifdefault_input >=0:default_prefix=devices[default_input]['name'].split('(')[0].strip()longest=''fordevindevices:name=dev['name']ifdev['max_input_channels'] >0:prefix=name.split('(')[0].strip()ifprefix==default_prefixandlen(name) >len(longest):longest=nameifnotlongest:longest=devices[default_input]['name']self.input_device_map['default']=longestinput_display_names.append('default (系统默认设备)')logging.info(f'系统默认输入设备用全称: {longest}')name_prefix_map={}fordevindevices:name=dev['name']ifdev['max_input_channels'] >0:prefix=name.split('(')[0].strip()if(prefixnotinname_prefix_map)or(len(name) >len(name_prefix_map[prefix])):name_prefix_map[prefix]=namefornameinsorted(name_prefix_map.values()):self.input_device_map[name]=nameinput_display_names.append(name)logging.info(f'最终输入设备: {name}')exceptException as e:logging.error(f'设备检测错误: {e}\n{traceback.format_exc()}')input_display_names=["default (系统默认设备)"]logging.info(f'最终输入设备列表: {input_display_names}')returninput_display_namesdefdetect_virtual_cable(self):fornameinself.input_devices:if"VB-Audio Virtual Cable"innameor"CABLE Output"innameor"CABLE Input"inname:returnTruereturnFalsedefsetup_ui(self):logging.debug('初始化UI界面')self.root=tk.Tk()ico_path=resource_path('wechat_recorder.ico')try:self.root.iconbitmap(ico_path)exceptException 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()ifnothas_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')ifcurrent_input=='default'and'default (系统默认设备)'inself.input_devices:self.input_device_combo.set('default (系统默认设备)')elifcurrent_inputinself.input_device_mapandcurrent_inputinself.input_devices:self.input_device_combo.set(current_input)elifself.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=Trueself._recording_in_progress=Falseself.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)defupdate_volume_bar(self):try:importsounddevice as sdimportnumpy as npifgetattr(self,'_recording_in_progress',False):self.volume_progressbar['value']=0self.volume_label['foreground']='gray'self.volume_label['text']='录音中'else:selected_name=self.input_device_combo.get()devices=sd.query_devices()matched_index=Noneforidx, devinenumerate(devices):ifselected_nameindev['name']anddev['max_input_channels'] >0:matched_index=idxbreak# 如果是“default (系统默认设备)”或找不到,就用默认设备ifselected_name=='default (系统默认设备)'ormatched_indexisNone:matched_index=Nonefs=16000duration=0.07data=sd.rec(int(duration*fs), samplerate=fs, channels=1, device=matched_index, blocking=True)ifdataisnotNoneanddata.any():rms=float(np.sqrt(np.mean(np.square(data))))percent=min(int(rms*4000),100)self.volume_progressbar['value']=percentself.volume_label['foreground']='black'self.volume_label['text']=f'{percent}%'else:self.volume_progressbar['value']=0self.volume_label['text']='0%'exceptException as e:importtracebacklogging.warning(f'音量获取失败: {e}\n{traceback.format_exc()}')self.volume_progressbar['value']=0self.volume_label['foreground']='black'self.volume_label['text']='0%'finally:ifgetattr(self,'_monitor_volume',False):self.root.after(200,self.update_volume_bar)deftest_record_and_play(self):fromdatetimeimportdatetimeself.test_record_btn['state']=tk.DISABLEDself.volume_label['text']='测试中'self._recording_in_progress=Trueself.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()ifselected_device=='default (系统默认设备)':input_name=self.input_device_map['default']else:input_name=selected_devicecmd=[self.ffmpeg_path,'-f','dshow','-i', f'audio={input_name}','-t','5','-acodec','libmp3lame','-y', filename]logging.info(f"测试录音命令: {' '.join(cmd)}")defrecord_and_play():try:p=subprocess.Popen(cmd,stdout=subprocess.PIPE,stderr=subprocess.PIPE,creationflags=getattr(subprocess,'CREATE_NO_WINDOW',0))p.wait()ifos.path.exists(filename):os.startfile(filename)else:messagebox.showerror("测试录音失败","未生成录音文件,请检查设备和设置。")exceptException as e:messagebox.showerror("测试录音失败", f"录音失败:{e}")finally:time.sleep(1)self.test_record_btn['state']=tk.NORMALself._recording_in_progress=Falseself.volume_label['text']='0%'threading.Thread(target=record_and_play, daemon=True).start()defsetup_tray_icon(self):logging.debug('初始化托盘图标')try:icon_path=resource_path('wechat_recorder.ico')image=Image.open(icon_path)exceptException: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)))defselect_path(self):path=filedialog.askdirectory()ifpath:logging.info(f'用户选择保存路径: {path}')self.path_entry.delete(0, tk.END)self.path_entry.insert(0, path)defsave_ui_config(self):selected_input=self.input_device_combo.get()self.config['input_device']='default'ifselected_input=='default (系统默认设备)'elseselected_inputself.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']}")defon_close(self):ifself.minimize_var.get()=='minimize':self.root.withdraw()ifnotself.icon.visible:threading.Thread(target=self.icon.run, name='TrayThread', daemon=True).start()logging.info("窗口最小化到托盘")else:self.quit_app()defshow_window(self,*args):logging.info('显示主窗口')try:self.root.deiconify()exceptException as e:logging.warning(f'主窗口显示异常: {e}')defquit_app(self,*args):logging.info('接收到退出请求,准备退出')try:ifhasattr(self,"icon")andself.icon.visible:self.icon.stop()exceptException as e:logging.warning(f'托盘退出异常: {e}')try:self.root.destroy()exceptException as e:logging.warning(f'窗口销毁异常: {e}')logging.info("程序退出")os._exit(0)defmonitor_wechat_window(self):logging.debug('启动微信窗口监控线程')whileTrue:try:all_windows=gw.getAllWindows()in_call=Falseforwinall_windows:try:cls=get_window_class(w._hWnd)ifclsinCALL_WINDOW_CLASSES:in_call=TruebreakexceptException:continuelogging.info(f"检测窗口: {'在通话' if in_call else '未通话'},录音状态: {self.recording}")ifin_callandnotself.recording:logging.debug('检测到通话窗口出现,准备开始录音')self.start_recording()elifnotin_callandself.recording:logging.debug('检测到通话窗口关闭,准备停止录音')self.stop_recording()time.sleep(1)exceptException as e:logging.error(f"监控微信窗口时出错: {e}\n{traceback.format_exc()}")time.sleep(5)defstart_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=filenameselected_device=self.input_device_combo.get()ifselected_device=='default (系统默认设备)':input_name=self.input_device_map['default']else:input_name=selected_devicecmd=[self.ffmpeg_path,'-f','dshow','-i', f'audio={input_name}','-acodec','libmp3lame','-y', filename]logging.info(f"本次录音命令为: {' '.join(cmd)}")self.recording=Trueself._recording_in_progress=Trueself.volume_label['foreground']='gray'self.volume_label['text']='录音中'self.volume_progressbar['value']=0self.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}")exceptException as e:logging.error(f"开始录音失败: {e}\n{traceback.format_exc()}")self.recording=Falseself._recording_in_progress=Falsedefstop_recording(self):ifself.recording_thread:try:ifself.recording_thread.stdin:try:self.recording_thread.stdin.write(b'q\n')self.recording_thread.stdin.flush()exceptException 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)ifstdout:logging.info(f'ffmpeg stdout: {stdout.decode("utf-8", "ignore")}')ifstderr:logging.info(f'ffmpeg stderr: {stderr.decode("utf-8", "ignore")}')exceptException as e:logging.warning(f'获取ffmpeg输出异常: {e}')exceptException as e:logging.warning(f"终止录音进程失败: {e}\n{traceback.format_exc()}")finally:self.recording_thread=Noneifhasattr(self,'last_record_file'):ifos.path.exists(self.last_record_file):logging.info(f"录音文件已生成: {self.last_record_file}")else:logging.error(f"录音进程结束但未发现录音文件: {self.last_record_file}")self.recording=Falseself._recording_in_progress=Falseself.volume_label['foreground']='black'self.update_volume_bar()logging.info("录音结束")defrun(self):logging.info('启动主线程,进入主循环')self.check_thread.start()self.root.mainloop()if__name__=='__main__':app=WeChatRecorder()app.run()
最后
这个工具比较小众,但希望它能帮到你。如果你也有自己的“小需求”,不妨动手试试。哪怕不全懂,有 AI 帮助,一切都变得简单了起来。
本工具纯属个人学习作品,尚未支持多线程录音、静音检测等高级功能,请酌情使用。如遇问题,欢迎理性反馈或共同改进。
