吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 942|回复: 15
收起左侧

[Windows] 微信语音发送器 v0.4.1

[复制链接]
Zero清风 发表于 2026-8-27 08:21
如何变成微信里面最靓的仔,本地音频轻松变成微信原生语音气泡!支持 WAV、MP3、OGG、FLAC 等常用格式。
软件不读取微信聊天记录、不操作联系人,不会自动点击发送,全程手动确认。
下载链接:https://azg.lanzouq.com/i92Eh44paymf
密码:52pj
附代码:


from __future__ import annotations

import ctypes
import json
import logging
import os
import platform
import sys
import threading
import time
import webbrowser
from dataclasses import dataclass
from pathlib import Path
from typing import Callable

APP_NAME = "微信语音发送器"
APP_NAME_EN = "WeChat Voice Sender"
APP_VERSION = "0.4.1"
SUPPORTED_SUFFIXES = {".wav", ".mp3", ".ogg", ".flac"}
MAX_AUDIO_SECONDS = 55.0
VK_F8 = 0x77
VK_F9 = 0x78
HOTKEY_POLL_MS = 30
PLAYBACK_CHUNK_SECONDS = 0.02
VB_CABLE_URL = "https://vb-audio.com/Cable/"

def app_data_dir() -> Path:
    base = Path(os.environ.get("LOCALAPPDATA", Path.home()))
    path = base / "WeChatVoiceSender"
    path.mkdir(parents=True, exist_ok=True)
    return path

APP_DATA_DIR = app_data_dir()
LOG_PATH = APP_DATA_DIR / "voice_sender.log"
SETTINGS_PATH = APP_DATA_DIR / "settings.json"

def resource_path(relative: str) -> Path:
    bundle_dir = Path(getattr(sys, "_MEIPASS", Path(__file__).resolve().parent))
    return bundle_dir / relative

def setup_windows_dpi() -> None:
    if platform.system() != "Windows":
        return
    try:
        ctypes.windll.shcore.SetProcessDpiAwareness(2)
    except Exception:
        try:
            ctypes.windll.user32.SetProcessDPIAware()
        except Exception:
            pass

def setup_logging() -> None:
    logging.basicConfig(
        level=logging.INFO,
        format="%(asctime)s | %(levelname)s | %(message)s",
        handlers=[logging.FileHandler(LOG_PATH, encoding="utf-8")],
        force=True,
    )

@dataclass(frozen=True)
class AudioEnvironment:
    cable_playback_index: int
    cable_playback_name: str
    cable_recording_index: int
    cable_recording_name: str

@dataclass(frozen=True)
class AudioSelection:
    path: Path
    duration: float

def load_dependencies():
    if platform.system() != "Windows":
        raise RuntimeError("此程序只能在 Windows 10/11 上运行。")
    try:
        import sounddevice as sd
        import soundfile as sf
    except ImportError as exc:
        if getattr(sys, "frozen", False):
            raise RuntimeError("程序组件不完整,请重新下载独立版。") from exc
        raise RuntimeError("依赖未安装,请运行“启动源码版.bat”。") from exc
    return sd, sf

def check_environment() -> tuple[AudioEnvironment, object, object]:
    sd, sf = load_dependencies()
    playback: list[tuple[int, str]] = []
    recording: list[tuple[int, str]] = []
    for index, device in enumerate(sd.query_devices()):
        name = str(device.get("name", ""))
        lowered = name.lower()
        if device.get("max_output_channels", 0) > 0 and "cable input" in lowered:
            playback.append((index, name))
        if device.get("max_input_channels", 0) > 0 and "cable output" in lowered:
            recording.append((index, name))
    if not playback or not recording:
        raise RuntimeError("未检测到 VB-CABLE,请安装驱动并重启电脑。")
    environment = AudioEnvironment(
        cable_playback_index=playback[0][0],
        cable_playback_name=playback[0][1],
        cable_recording_index=recording[0][0],
        cable_recording_name=recording[0][1],
    )
    return environment, sd, sf

def validate_audio(audio_path: str, sf) -> AudioSelection:
    if not audio_path.strip():
        raise ValueError("请先选择本地音频文件。")
    path = Path(os.path.expandvars(os.path.expanduser(audio_path))).resolve()
    if not path.is_file():
        raise ValueError("请选择存在的本地音频文件。")
    if path.suffix.lower() not in SUPPORTED_SUFFIXES:
        raise ValueError("当前支持 WAV、MP3、OGG、FLAC 音频。")
    try:
        info = sf.info(str(path))
        duration = float(info.frames) / float(info.samplerate)
    except Exception as exc:
        raise ValueError(f"无法读取音频文件:{exc}") from exc
    if duration <= 0:
        raise ValueError("音频文件为空。")
    if duration > MAX_AUDIO_SECONDS:
        raise ValueError(
            f"音频时长为 {duration:.1f} 秒,当前上限为 {MAX_AUDIO_SECONDS:.0f} 秒。"
            "请先裁剪,避免触发微信录音时长限制。"
        )
    return AudioSelection(path=path, duration=duration)

def load_audio(audio_path: Path, sf):
    try:
        audio, samplerate = sf.read(str(audio_path), dtype="float32", always_2d=True)
    except Exception as exc:
        raise RuntimeError(f"无法解码音频文件:{exc}") from exc
    if samplerate <= 0 or len(audio) == 0:
        raise RuntimeError("音频文件为空或采样率无效。")
    if audio.shape[1] > 2:
        audio = audio[:, :2]
    return audio, samplerate

def play_to_cable(
    selection: AudioSelection,
    status: Callable[[str], None],
    progress: Callable[[float], None],
    stop_event: threading.Event,
) -> bool:
    environment, sd, sf = check_environment()
    audio, samplerate = load_audio(selection.path, sf)
    interrupted = False
    total_frames = len(audio)
    elapsed_tick = 1
    try:
        status("正在向 VB-CABLE 播放;需要提前结束时按 F9。")
        status("播放计时:0 秒")
        chunk_frames = max(256, int(samplerate * PLAYBACK_CHUNK_SECONDS))
        with sd.OutputStream(
            samplerate=samplerate,
            device=environment.cable_playback_index,
            channels=audio.shape[1],
            dtype="float32",
        ) as stream:
            for start in range(0, total_frames, chunk_frames):
                if stop_event.is_set():
                    interrupted = True
                    break
                end = min(start + chunk_frames, total_frames)
                stream.write(audio[start:end])
                elapsed_seconds = end / samplerate
                while elapsed_seconds >= elapsed_tick:
                    status(f"播放计时:{elapsed_tick} 秒")
                    elapsed_tick += 1
                progress(end / total_frames)
        time.sleep(0.12)
        if interrupted:
            status("已提前结束。请返回微信确认试听,并手动点击发送。")
        else:
            progress(1.0)
            status(f"播放计时结束:{selection.duration:.1f} 秒")
            status("播放完成。请返回微信确认试听,并手动点击发送。")
        try:
            import winsound

            winsound.MessageBeep(winsound.MB_OK)
        except Exception:
            pass
        return interrupted
    except Exception:
        try:
            sd.stop()
        except Exception:
            pass
        raise

class VoicePlayerApp:
    COLORS = {
        "blue": "#0F6CBD",
        "blue_hover": "#115EA3",
        "green": "#107C10",
        "red": "#C42B1C",
    }

    def __init__(self) -> None:
        import customtkinter as ctk
        import tkinter as tk
        from tkinter import filedialog, messagebox

        self.ctk = ctk
        self.tk = tk
        self.filedialog = filedialog
        self.messagebox = messagebox
        self.worker: threading.Thread | None = None
        self.stop_event = threading.Event()
        self.waiting_for_f8 = False
        self.pending_audio: AudioSelection | None = None
        self.playback_duration = 0.0
        self.f8_was_down = False
        self.f9_was_down = False
        self.last_folder = ""

        settings = self.load_settings()
        theme = str(settings.get("theme", "System"))
        if theme not in {"System", "Light", "Dark"}:
            theme = "System"
        self.last_folder = str(settings.get("last_folder", ""))
        ctk.set_appearance_mode(theme)
        ctk.set_default_color_theme("blue")

        self.root = ctk.CTk()
        self.root.title(f"{APP_NAME}  ·  v{APP_VERSION}")
        self.root.geometry("960x680")
        self.root.minsize(900, 640)
        self.root.protocol("WM_DELETE_WINDOW", self.on_close)
        self.root.grid_columnconfigure(0, weight=1)
        self.root.grid_rowconfigure(1, weight=1)

        icon_path = resource_path("assets/app_icon.ico")
        if icon_path.exists():
            try:
                self.root.iconbitmap(str(icon_path))
            except Exception:
                pass

        self.audio_var = tk.StringVar()
        self.theme_var = tk.StringVar(value=theme)
        self.file_name_var = tk.StringVar(value="尚未选择音频")
        self.file_meta_var = tk.StringVar(value="支持 WAV / MP3 / OGG / FLAC,最长 55 秒")
        self.device_var = tk.StringVar(value="尚未检测 VB-CABLE")
        self.progress_text_var = tk.StringVar(value="等待选择音频")

        self.build_header()
        self.build_content()
        self.root.after(120, self.apply_windows_style)
        self.root.after(HOTKEY_POLL_MS, self.poll_hotkeys)
        self.append_status("程序已启动。选择音频后先检测 VB-CABLE。")

    def build_header(self) -> None:
        ctk = self.ctk
        header = ctk.CTkFrame(self.root, fg_color="transparent", corner_radius=0)
        header.grid(row=0, column=0, sticky="ew", padx=28, pady=(22, 12))
        header.grid_columnconfigure(1, weight=1)

        ctk.CTkLabel(
            header,
            text="W",
            width=44,
            height=44,
            corner_radius=12,
            fg_color=self.COLORS["blue"],
            text_color="white",
            font=ctk.CTkFont(family="Segoe UI Variable Display", size=21, weight="bold"),
        ).grid(row=0, column=0, rowspan=2, padx=(0, 13))
        ctk.CTkLabel(
            header,
            text=APP_NAME,
            anchor="w",
            font=ctk.CTkFont(family="Segoe UI Variable Display", size=24, weight="bold"),
        ).grid(row=0, column=1, sticky="sw")
        ctk.CTkLabel(
            header,
            text="把本地音频送入微信语音输入 · 手动确认发送",
            anchor="w",
            text_color=("#5F5F5F", "#B8B8B8"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12),
        ).grid(row=1, column=1, sticky="nw")
        ctk.CTkLabel(
            header,
            text=f"v{APP_VERSION}  独立版",
            height=28,
            corner_radius=14,
            fg_color=("#E5F1FB", "#14344A"),
            text_color=("#0F6CBD", "#62B8F6"),
            font=ctk.CTkFont(family="Segoe UI", size=11, weight="bold"),
        ).grid(row=0, column=2, rowspan=2, padx=(8, 12))
        self.theme_menu = ctk.CTkOptionMenu(
            header,
            values=["System", "Light", "Dark"],
            variable=self.theme_var,
            command=self.change_theme,
            width=104,
            height=32,
            corner_radius=8,
            fg_color=("#F0F0F0", "#303030"),
            button_color=("#E4E4E4", "#3A3A3A"),
            button_hover_color=("#D8D8D8", "#464646"),
            text_color=("#202020", "#F4F4F4"),
        )
        self.theme_menu.grid(row=0, column=3, rowspan=2)

    def build_content(self) -> None:
        ctk = self.ctk
        body = ctk.CTkFrame(self.root, fg_color="transparent")
        body.grid(row=1, column=0, sticky="nsew", padx=28, pady=(0, 24))
        body.grid_columnconfigure(0, weight=3, uniform="body")
        body.grid_columnconfigure(1, weight=2, uniform="body")
        body.grid_rowconfigure(0, weight=1)

        left = ctk.CTkFrame(
            body, corner_radius=16, border_width=1,
            border_color=("#E3E3E3", "#363636"), fg_color=("#FFFFFF", "#242424")
        )
        left.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
        left.grid_columnconfigure(0, weight=1)
        left.grid_rowconfigure(7, weight=1)
        ctk.CTkLabel(
            left, text="音频与控制", anchor="w",
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=17, weight="bold")
        ).grid(row=0, column=0, sticky="ew", padx=22, pady=(20, 4))
        ctk.CTkLabel(
            left, text="选择一段音频,程序会将它播放到 CABLE Input。", anchor="w",
            text_color=("#666666", "#B7B7B7"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12)
        ).grid(row=1, column=0, sticky="ew", padx=22, pady=(0, 16))

        picker = ctk.CTkFrame(left, fg_color=("#F7F7F7", "#2D2D2D"), corner_radius=12)
        picker.grid(row=2, column=0, sticky="ew", padx=22)
        picker.grid_columnconfigure(0, weight=1)
        ctk.CTkLabel(
            picker, textvariable=self.file_name_var, anchor="w",
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=14, weight="bold")
        ).grid(row=0, column=0, sticky="ew", padx=(16, 8), pady=(13, 2))
        ctk.CTkLabel(
            picker, textvariable=self.file_meta_var, anchor="w",
            text_color=("#6A6A6A", "#B5B5B5"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=11)
        ).grid(row=1, column=0, sticky="ew", padx=(16, 8), pady=(0, 13))
        ctk.CTkButton(
            picker, text="选择音频…", command=self.choose_audio, width=105, height=34,
            corner_radius=8, fg_color=self.COLORS["blue"], hover_color=self.COLORS["blue_hover"],
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12, weight="bold")
        ).grid(row=0, column=1, rowspan=2, padx=(6, 14), pady=14)

        self.path_entry = ctk.CTkEntry(
            left, textvariable=self.audio_var, height=36, corner_radius=8,
            placeholder_text="也可以粘贴完整音频路径",
            border_color=("#D2D2D2", "#484848"),
            font=ctk.CTkFont(family="Segoe UI", size=11)
        )
        self.path_entry.grid(row=3, column=0, sticky="ew", padx=22, pady=(10, 16))
        self.path_entry.bind("<FocusOut>", lambda _event: self.inspect_selected_audio(False))

        device_row = ctk.CTkFrame(left, fg_color="transparent")
        device_row.grid(row=4, column=0, sticky="ew", padx=22)
        device_row.grid_columnconfigure(0, weight=1)
        self.device_label = ctk.CTkLabel(
            device_row, textvariable=self.device_var, anchor="w",
            text_color=("#666666", "#B7B7B7"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=11)
        )
        self.device_label.grid(row=0, column=0, sticky="ew")
        ctk.CTkButton(
            device_row, text="检测设备", command=self.detect_environment,
            width=92, height=30, corner_radius=8,
            fg_color=("#EAEAEA", "#343434"), hover_color=("#DDDDDD", "#414141"),
            text_color=("#202020", "#F4F4F4")
        ).grid(row=0, column=1, padx=(8, 0))

        self.progress_bar = ctk.CTkProgressBar(
            left, height=8, corner_radius=4, progress_color=self.COLORS["blue"],
            fg_color=("#EAEAEA", "#383838")
        )
        self.progress_bar.grid(row=5, column=0, sticky="ew", padx=22, pady=(20, 5))
        self.progress_bar.set(0)
        ctk.CTkLabel(
            left, textvariable=self.progress_text_var, anchor="w",
            text_color=("#5F5F5F", "#BCBCBC"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=11)
        ).grid(row=6, column=0, sticky="ew", padx=22)

        self.status_text = ctk.CTkTextbox(
            left, height=126, corner_radius=10, border_width=1,
            border_color=("#E4E4E4", "#3A3A3A"), fg_color=("#FAFAFA", "#1E1E1E"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=11), wrap="word"
        )
        self.status_text.grid(row=7, column=0, sticky="nsew", padx=22, pady=(13, 14))
        self.status_text.configure(state="disabled")

        controls = ctk.CTkFrame(left, fg_color="transparent")
        controls.grid(row=8, column=0, sticky="ew", padx=22, pady=(0, 20))
        controls.grid_columnconfigure(0, weight=1)
        self.play_button = ctk.CTkButton(
            controls, text="进入等待状态  ·  F8 播放", command=self.arm_f8_playback,
            height=42, corner_radius=10, fg_color=self.COLORS["blue"],
            hover_color=self.COLORS["blue_hover"],
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=13, weight="bold")
        )
        self.play_button.grid(row=0, column=0, sticky="ew")
        self.cancel_button = ctk.CTkButton(
            controls, text="取消等待", command=self.cancel_waiting, width=98, height=42,
            corner_radius=10, state="disabled", fg_color=("#E8E8E8", "#353535"),
            hover_color=("#DADADA", "#424242"), text_color=("#202020", "#F4F4F4")
        )
        self.cancel_button.grid(row=0, column=1, padx=(9, 0))
        self.stop_button = ctk.CTkButton(
            controls, text="停止  F9", command=self.request_stop_playback, width=92, height=42,
            corner_radius=10, state="disabled", fg_color=self.COLORS["red"], hover_color="#A4262C"
        )
        self.stop_button.grid(row=0, column=2, padx=(9, 0))

        right = ctk.CTkFrame(
            body, corner_radius=16, border_width=1,
            border_color=("#E3E3E3", "#363636"), fg_color=("#FFFFFF", "#242424")
        )
        right.grid(row=0, column=1, sticky="nsew", padx=(10, 0))
        right.grid_columnconfigure(0, weight=1)
        ctk.CTkLabel(
            right, text="发送流程", anchor="w",
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=17, weight="bold")
        ).grid(row=0, column=0, sticky="ew", padx=22, pady=(20, 4))
        ctk.CTkLabel(
            right, text="全程由你确认,不读取微信数据。", anchor="w",
            text_color=("#666666", "#B7B7B7"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12)
        ).grid(row=1, column=0, sticky="ew", padx=22, pady=(0, 14))

        steps = [
            ("1", "准备微信", "打开正确好友或群聊,点击微信的语音输入。"),
            ("2", "开始播放", "本软件进入等待后,在微信录音界面按 F8。"),
            ("3", "确认发送", "听到提示音后,返回微信手动点击绿色发送箭头。"),
        ]
        for row, (number, title, detail) in enumerate(steps, start=2):
            self.add_step_card(right, row, number, title, detail)

        tip = ctk.CTkFrame(right, fg_color=("#F5F9FD", "#172B3A"), corner_radius=12)
        tip.grid(row=5, column=0, sticky="ew", padx=22, pady=(16, 10))
        ctk.CTkLabel(
            tip, text="快捷键", anchor="w", text_color=("#0F6CBD", "#62B8F6"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12, weight="bold")
        ).grid(row=0, column=0, sticky="ew", padx=14, pady=(12, 4))
        ctk.CTkLabel(
            tip, text="F8  开始播放     F9  提前结束", anchor="w",
            font=ctk.CTkFont(family="Segoe UI", size=12)
        ).grid(row=1, column=0, sticky="ew", padx=14, pady=(0, 12))

        links = ctk.CTkFrame(right, fg_color="transparent")
        links.grid(row=6, column=0, sticky="ew", padx=22, pady=(4, 18))
        links.grid_columnconfigure(0, weight=1)
        links.grid_columnconfigure(1, weight=1)
        ctk.CTkButton(
            links, text="VB-CABLE 官网", command=lambda: webbrowser.open(VB_CABLE_URL),
            height=34, corner_radius=8, fg_color="transparent",
            hover_color=("#EEEEEE", "#333333"), text_color=("#0F6CBD", "#62B8F6"),
            border_width=1, border_color=("#D6D6D6", "#484848")
        ).grid(row=0, column=0, sticky="ew", padx=(0, 5))
        ctk.CTkButton(
            links, text="打开日志目录", command=self.open_log_folder,
            height=34, corner_radius=8, fg_color="transparent",
            hover_color=("#EEEEEE", "#333333"), text_color=("#202020", "#F4F4F4"),
            border_width=1, border_color=("#D6D6D6", "#484848")
        ).grid(row=0, column=1, sticky="ew", padx=(5, 0))

    def add_step_card(self, parent, row: int, number: str, title: str, detail: str) -> None:
        ctk = self.ctk
        card = ctk.CTkFrame(parent, fg_color=("#F7F7F7", "#2D2D2D"), corner_radius=12)
        card.grid(row=row, column=0, sticky="ew", padx=22, pady=5)
        card.grid_columnconfigure(1, weight=1)
        ctk.CTkLabel(
            card, text=number, width=30, height=30, corner_radius=15,
            fg_color=("#E5F1FB", "#14344A"), text_color=("#0F6CBD", "#62B8F6"),
            font=ctk.CTkFont(family="Segoe UI", size=12, weight="bold")
        ).grid(row=0, column=0, rowspan=2, padx=(13, 11), pady=13)
        ctk.CTkLabel(
            card, text=title, anchor="w",
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=12, weight="bold")
        ).grid(row=0, column=1, sticky="sew", padx=(0, 12), pady=(11, 1))
        ctk.CTkLabel(
            card, text=detail, anchor="w", justify="left", wraplength=260,
            text_color=("#666666", "#B7B7B7"),
            font=ctk.CTkFont(family="Microsoft YaHei UI", size=10)
        ).grid(row=1, column=1, sticky="new", padx=(0, 12), pady=(1, 11))

    def append_status(self, text: str) -> None:
        logging.info(text)

        def update() -> None:
            stamp = time.strftime("%H:%M:%S")
            self.status_text.configure(state="normal")
            self.status_text.insert("end", f"{stamp}  {text}\n")
            self.status_text.see("end")
            self.status_text.configure(state="disabled")

        self.root.after(0, update)

    def update_progress(self, value: float) -> None:
        value = min(1.0, max(0.0, float(value)))
        duration = self.playback_duration
        elapsed = min(duration, value * duration)

        def update() -> None:
            self.progress_bar.set(value)
            self.progress_text_var.set(
                f"正在播放  {elapsed:.1f} / {duration:.1f} 秒  ·  {round(value * 100):d}%"
            )

        self.root.after(0, update)

    def choose_audio(self) -> None:
        initial_dir = self.last_folder if Path(self.last_folder).is_dir() else None
        path = self.filedialog.askopenfilename(
            title="选择本地音频", initialdir=initial_dir,
            filetypes=[("支持的音频", "*.wav *.mp3 *.ogg *.flac"), ("所有文件", "*.*")]
        )
        if not path:
            return
        self.audio_var.set(path)
        self.last_folder = str(Path(path).parent)
        self.save_settings()
        self.inspect_selected_audio(show_error=True)

    def inspect_selected_audio(self, show_error: bool = True) -> AudioSelection | None:
        if not self.audio_var.get().strip():
            return None
        try:
            _sd, sf = load_dependencies()
            selection = validate_audio(self.audio_var.get(), sf)
            self.file_name_var.set(selection.path.name)
            size_mb = selection.path.stat().st_size / (1024 * 1024)
            self.file_meta_var.set(
                f"{selection.duration:.1f} 秒  ·  {size_mb:.2f} MB  ·  {selection.path.suffix[1:].upper()}"
            )
            self.progress_text_var.set("音频已就绪")
            return selection
        except Exception as exc:
            self.file_name_var.set("音频不可用")
            self.file_meta_var.set(str(exc))
            if show_error:
                self.messagebox.showerror(APP_NAME, str(exc))
            return None

    def detect_environment(self) -> bool:
        try:
            environment, _sd, _sf = check_environment()
            self.device_var.set(f"已连接 · {environment.cable_playback_name}")
            self.device_label.configure(text_color=(self.COLORS["green"], "#6CCB5F"))
            self.append_status(f"VB-CABLE 检测通过:{environment.cable_playback_name}")
            return True
        except Exception as exc:
            logging.exception("环境检测失败")
            self.device_var.set("VB-CABLE 未就绪")
            self.device_label.configure(text_color=(self.COLORS["red"], "#FF8177"))
            self.append_status(f"环境检测失败:{exc}")
            self.messagebox.showerror(
                APP_NAME,
                f"{exc}\n\n请安装 VB-CABLE、重启电脑,并将 CABLE Output 设为默认录音设备。"
            )
            return False

    def arm_f8_playback(self) -> None:
        if self.worker and self.worker.is_alive():
            self.messagebox.showwarning(APP_NAME, "音频正在播放。")
            return
        if self.waiting_for_f8:
            self.messagebox.showwarning(APP_NAME, "程序已经在等待 F8。")
            return
        selection = self.inspect_selected_audio(show_error=True)
        if selection is None or not self.detect_environment():
            return
        confirmed = self.messagebox.askyesno(
            "准备发送语音",
            f"音频:{selection.path.name}\n时长:{selection.duration:.1f} 秒\n\n"
            "确认后窗口保持打开并等待 F8。\n"
            "请在微信点击“语音输入”,再按 F8 开始播放。"
        )
        if not confirmed:
            return
        self.pending_audio = selection
        self.waiting_for_f8 = True
        self.f8_was_down = self.is_key_down(VK_F8)
        self.play_button.configure(state="disabled")
        self.cancel_button.configure(state="normal")
        self.progress_bar.set(0)
        self.progress_text_var.set("等待 F8 · 窗口保持打开")
        self.append_status(f"已进入等待状态:{selection.path.name};窗口保持打开。")

    @staticmethod
    def is_key_down(virtual_key: int) -> bool:
        if platform.system() != "Windows":
            return False
        return bool(ctypes.windll.user32.GetAsyncKeyState(virtual_key) & 0x8000)

    def poll_hotkeys(self) -> None:
        try:
            if self.waiting_for_f8:
                is_down = self.is_key_down(VK_F8)
                if is_down and not self.f8_was_down:
                    self.start_playback_from_f8()
                self.f8_was_down = is_down
            else:
                self.f8_was_down = False
            if self.worker and self.worker.is_alive():
                is_down = self.is_key_down(VK_F9)
                if is_down and not self.f9_was_down:
                    self.request_stop_playback()
                self.f9_was_down = is_down
            else:
                self.f9_was_down = False
        except Exception as exc:
            logging.exception("快捷键检测失败")
            self.cancel_waiting()
            self.messagebox.showerror(APP_NAME, f"无法检测 F8/F9:{exc}")
        finally:
            self.root.after(HOTKEY_POLL_MS, self.poll_hotkeys)

    def cancel_waiting(self) -> None:
        if not self.waiting_for_f8:
            return
        self.waiting_for_f8 = False
        self.pending_audio = None
        self.play_button.configure(state="normal")
        self.cancel_button.configure(state="disabled")
        self.progress_text_var.set("已取消等待")
        self.append_status("已取消等待 F8,没有播放音频。")

    def start_playback_from_f8(self) -> None:
        selection = self.pending_audio
        if not self.waiting_for_f8 or selection is None:
            return
        self.waiting_for_f8 = False
        self.pending_audio = None
        self.cancel_button.configure(state="disabled")
        self.stop_event.clear()
        self.playback_duration = selection.duration
        self.f9_was_down = self.is_key_down(VK_F9)
        self.stop_button.configure(state="normal")
        self.progress_text_var.set(
            f"正在播放  0.0 / {selection.duration:.1f} 秒  ·  0%"
        )
        self.append_status("检测到 F8,开始播放。")

        def task() -> None:
            try:
                interrupted = play_to_cable(
                    selection, self.append_status, self.update_progress, self.stop_event
                )
                text = "已提前结束 · 返回微信确认" if interrupted else "播放完成 · 返回微信确认"
                self.root.after(0, lambda value=text: self.progress_text_var.set(value))
            except Exception as exc:
                logging.exception("音频播放失败")
                self.append_status(f"播放失败:{exc}")
                message = str(exc)
                self.root.after(
                    0,
                    lambda error_message=message: self.messagebox.showerror(
                        APP_NAME, f"{error_message}\n\n诊断日志:\n{LOG_PATH}"
                    )
                )
            finally:
                self.root.after(0, self.reset_controls_after_playback)

        self.worker = threading.Thread(target=task, daemon=True)
        self.worker.start()

    def reset_controls_after_playback(self) -> None:
        self.play_button.configure(state="normal")
        self.stop_button.configure(state="disabled")

    def request_stop_playback(self) -> None:
        if not self.worker or not self.worker.is_alive() or self.stop_event.is_set():
            return
        self.stop_event.set()
        self.stop_button.configure(state="disabled")
        self.append_status("检测到 F9,正在提前结束……")

    def change_theme(self, value: str) -> None:
        self.ctk.set_appearance_mode(value)
        self.save_settings()
        self.root.after(80, self.apply_windows_style)

    def apply_windows_style(self) -> None:
        if platform.system() != "Windows":
            return
        try:
            self.root.update_idletasks()
            hwnd = self.root.winfo_id()
            dark = ctypes.c_int(1 if self.ctk.get_appearance_mode() == "Dark" else 0)
            corner = ctypes.c_int(2)
            ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 20, ctypes.byref(dark), 4)
            ctypes.windll.dwmapi.DwmSetWindowAttribute(hwnd, 33, ctypes.byref(corner), 4)
        except Exception:
            pass

    def open_log_folder(self) -> None:
        try:
            os.startfile(APP_DATA_DIR)  # type: ignore[attr-defined]
        except Exception as exc:
            self.messagebox.showerror(APP_NAME, f"无法打开日志目录:{exc}")

    def load_settings(self) -> dict:
        try:
            if SETTINGS_PATH.is_file():
                return json.loads(SETTINGS_PATH.read_text(encoding="utf-8"))
        except Exception:
            logging.exception("读取设置失败")
        return {}

    def save_settings(self) -> None:
        try:
            SETTINGS_PATH.write_text(
                json.dumps(
                    {"theme": self.theme_var.get(), "last_folder": self.last_folder},
                    ensure_ascii=False, indent=2
                ),
                encoding="utf-8"
            )
        except Exception:
            logging.exception("保存设置失败")

    def on_close(self) -> None:
        if self.worker and self.worker.is_alive():
            if not self.messagebox.askyesno(APP_NAME, "音频仍在播放,确定退出吗?"):
                return
            self.stop_event.set()
        self.save_settings()
        self.root.destroy()

    def run(self) -> None:
        self.root.mainloop()

def main() -> int:
    setup_windows_dpi()
    setup_logging()
    try:
        VoicePlayerApp().run()
    except Exception as exc:
        logging.exception("程序启动失败")
        try:
            import tkinter.messagebox as messagebox

            messagebox.showerror(APP_NAME_EN, f"程序启动失败:{exc}\n\n日志:{LOG_PATH}")
        except Exception:
            pass
        return 1
    return 0

if __name__ == "__main__":
    raise SystemExit(main())

安装与配置

安装与配置

操作说明

操作说明

操作流程

操作流程

软件界面

软件界面

免费评分

参与人数 2吾爱币 +2 热心值 +2 收起 理由
rmao189 + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
zhu086 + 1 + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

xiaoshuimian 发表于 2026-8-27 10:55
会封号吗?
jun269 发表于 2026-8-27 11:12
不好意思楼主,我还是没明白您这个到底是啥作用
feia 发表于 2026-8-27 11:23
确实不太明白,看流程是播放本地音频,微信录个音发送?
yueyegufeng 发表于 2026-8-27 11:53
搞诈骗的人会感觉很实用,好工具容易被坏心思的人拿去用。
 楼主| Zero清风 发表于 2026-8-27 12:01

没动微信的东西,实际就是接管了电脑的麦克风
 楼主| Zero清风 发表于 2026-8-27 12:05
feia 发表于 2026-8-27 11:23
确实不太明白,看流程是播放本地音频,微信录个音发送?

实际就是本地选一个声音或者音乐文件,然后发送
 楼主| Zero清风 发表于 2026-8-27 12:07
yueyegufeng 发表于 2026-8-27 11:53
搞诈骗的人会感觉很实用,好工具容易被坏心思的人拿去用。

pz都用声卡实时的变声,SVC,RVC啥的,猝不及防
头像被屏蔽
5222994 发表于 2026-8-27 12:09
提示: 该帖被管理员或版主屏蔽
ephemerality 发表于 2026-8-27 12:11
感谢大佬
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-8-28 08:09

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表