吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 938|回复: 56
上一主题 下一主题
收起左侧

[学习记录] MP4音轨分离与合成小工具(人声+伴奏)

  [复制链接]
跳转到指定楼层
楼主
psqladm 发表于 2026-4-28 06:24 回帖奖励
本帖最后由 psqladm 于 2026-4-28 08:35 编辑

    将MP4文件的混音,经模型识别后,单独拆分为人声+伴奏,保留混音为缺省音轨,再增加一条伴奏音轨,输出为双音轨MKV文件。

运行依赖:
1、FFMPEG
ffmpeg.exe文件的路径,添加到程序。
2、demucs
用于识别和分离音轨,pip install demucs安装,第一次运行程序时,会自动下载模型到本地。

分享一种编程方法,不包含任何版权资料,仅供个人娱乐。
想换个头像,还差点意思,有愿意帮忙的吗?感谢!

[Python] 纯文本查看 复制代码
#!/usr/bin/env python
# -*- coding: utf-8 -*-

import os
import queue
import shutil
import subprocess
import tempfile
import threading
import tkinter as tk
from pathlib import Path
from tkinter import ttk, filedialog, messagebox, scrolledtext

# 修改为您的实际文件路径
FFMPEG_PATH = r"C:\ffmpeg\bin\ffmpeg.exe"


class DualAudioApp:
    def __init__(self, root):
        self.root = root
        self.root.title("MP4 双音轨工具 (人声/伴奏)")
        self.root.geometry("780x480")
        self.root.resizable(False, False)

        self.selected_mp4 = None
        self.is_processing = False
        self.task_queue = queue.Queue()
        self.temp_dir = tempfile.mkdtemp(prefix="dual_audio_")

        self.ffmpeg_exe = FFMPEG_PATH

        self.out_dir_var = tk.StringVar(value="")
        self.out_dir_default = ""

        self._build_ui()
        self._start_ui_updater()

    def _build_ui(self):
        # 1. 文件选择区
        file_frame = ttk.Frame(self.root, padding=(15, 15, 15, 5))
        file_frame.pack(fill="x")
        ttk.Label(file_frame, text="📁 目标 MP4:").pack(side="left", padx=(0, 5))
        self.file_var = tk.StringVar(value="未选择文件")
        ttk.Entry(file_frame, textvariable=self.file_var, state="readonly", width=45).pack(side="left", fill="x",
                                                                                     expand=True, padx=5)
        ttk.Button(file_frame, text="选择文件", command=self._select_file).pack(side="left")

        # 2. 输出目录区
        out_frame = ttk.Frame(self.root, padding=(15, 5))
        out_frame.pack(fill="x")
        ttk.Label(out_frame, text="📂 输出目录:").pack(side="left", padx=(0, 5))
        ttk.Entry(out_frame, textvariable=self.out_dir_var, width=45).pack(side="left", fill="x", expand=True, padx=5)
        self.btn_select_out = ttk.Button(out_frame, text="选择目录", command=self._select_out_dir)
        self.btn_select_out.pack(side="left")
        ttk.Label(out_frame, text="(留空则默认保存至原文件同级目录)", foreground="gray",
                  font=("Microsoft YaHei", 8)).pack(side="left", padx=(8, 0))

        # 3. 控制按钮区
        ctrl_frame = ttk.Frame(self.root, padding=(15, 5))
        ctrl_frame.pack(fill="x")
        self.start_btn = ttk.Button(ctrl_frame, text="🚀 开始处理", command=self._start_processing, state="disabled")
        self.start_btn.pack(side="left")
        ttk.Label(ctrl_frame, text="💡 合成格式:原视频音轨 + AI分离伴奏音轨", foreground="gray",
                  font=("Microsoft YaHei", 9)).pack(side="left", padx=(10, 0))

        # 4. 进度条 & 日志
        prog_frame = ttk.Frame(self.root, padding=(15, 0))
        prog_frame.pack(fill="x")
        self.progress = ttk.Progressbar(prog_frame, mode="indeterminate", length=400)
        self.progress.pack(fill="x")

        log_frame = ttk.LabelFrame(self.root, text="运行日志", padding=(15, 5))
        log_frame.pack(fill="both", expand=True, padx=15, pady=10)
        self.log_text = scrolledtext.ScrolledText(log_frame, height=9, state="disabled", font=("Consolas", 9))
        self.log_text.pack(fill="both", expand=True)

    
    def _select_file(self):
        file = filedialog.askopenfilename(title="选择 MP4 视频文件",
                                          filetypes=[("MP4 视频", "*.mp4"), ("所有文件", "*.*")])
        if file:
            self.selected_mp4 = file
            self.file_var.set(file)
            # 默认输出目录设为原文件所在目录
            self.out_dir_default = str(Path(file).parent)
            if not self.out_dir_var.get().strip():
                self.out_dir_var.set(self.out_dir_default)
            self._update_start_btn_state()
            self._log("📥 已选择: " + file)

    def _select_out_dir(self):
        path = filedialog.askdirectory(title="选择输出目录",
                                       initialdir=self.out_dir_var.get().strip() or self.out_dir_default)
        if path:
            self.out_dir_var.set(path)

    def _update_start_btn_state(self):
        
        if self.selected_mp4:
            self.start_btn.config(state="normal")
        else:
            self.start_btn.config(state="disabled")

    def _log(self, msg):
        self.task_queue.put(("log", msg))

    
    def _start_ui_updater(self):
        def update():
            try:
                while True:
                    msg_type, content = self.task_queue.get_nowait()
                    if msg_type == "log":
                        self.log_text.config(state="normal")
                        self.log_text.insert("end", content + "\n")
                        self.log_text.see("end")
                        self.log_text.config(state="disabled")
                    elif msg_type == "progress_start":
                        self.progress.config(mode="indeterminate")
                        self.progress.start(10)
                    elif msg_type == "progress_stop":
                        self.progress.stop()
                        self.progress.config(mode="determinate", value=content)
                    elif msg_type == "done":
                        self._unlock_ui()
                        messagebox.showinfo("成功", content)
                    elif msg_type == "error":
                        self._unlock_ui()
                        messagebox.showerror("错误", content)
            except queue.Empty:
                pass
            self.root.after(100, update)

        update()

    def _lock_ui(self):
        self.is_processing = True
        self.start_btn.config(state="disabled", text="⏳ 处理中...")
        self.btn_select_out.config(state="disabled")

    def _unlock_ui(self):
        self.is_processing = False
        self.start_btn.config(state="normal", text="🚀 开始处理")
        self.btn_select_out.config(state="normal")
        self._update_start_btn_state()

    def _start_processing(self):
        if not self.selected_mp4 or self.is_processing: return

        # 启动前确认路径是否有效
        if not os.path.isfile(self.ffmpeg_exe):
            messagebox.showerror("配置错误",
                                 f"文件 FFmpeg 路径不存在或无效:\n{self.ffmpeg_exe}\n\n请修改代码中的 FFMPEG_PATH 变量。")
            return

        self._lock_ui()
        self.task_queue.put(("progress_start", None))
        self._log("🔄 初始化任务队列...")
        threading.Thread(target=self._worker, daemon=True).start()

    
    def _worker(self):
        try:
            self._log("🤖 步骤 1/2: 启动 AI 分离模型...")
            out_dir = Path(self.temp_dir) / "demucs_out"
            out_dir.mkdir(parents=True, exist_ok=True)

            # 环境变量注入(确保 demucs 能调用到 ffmpeg)
            ffmpeg_dir = os.path.dirname(os.path.abspath(self.ffmpeg_exe))
            custom_env = os.environ.copy()
            if ffmpeg_dir and os.path.isdir(ffmpeg_dir):
                custom_env["PATH"] = ffmpeg_dir + os.pathsep + custom_env.get("PATH", "")

            cmd_demucs = [
                "demucs", "--two-stems=vocals", "--name", "htdemucs",
                "--device", "cpu", "-o", str(out_dir), self.selected_mp4
            ]

            proc = subprocess.run(
                cmd_demucs, capture_output=True, text=True, check=True,
                encoding="utf-8", errors="ignore", env=custom_env,
                cwd=os.path.dirname(self.selected_mp4)
            )
            if proc.stdout or proc.stderr:
                log_lines = (proc.stdout + proc.stderr).splitlines()
                for line in log_lines:
                    if "%" in line or "ETA" in line: self._log(f"   {line.strip()}")

            self._log("✅ AI 分离完成")
            self.task_queue.put(("progress_stop", 50))

            audio_name = Path(self.selected_mp4).stem
            model_dir = out_dir / "htdemucs" / audio_name
            if not model_dir.exists(): model_dir = out_dir / "htdemucs"

            # 仅提取伴奏音轨 (no_vocals 即为分离后的伴奏)
            accompaniment = model_dir / "no_vocals.wav"
            if not accompaniment.exists():
                raise RuntimeError("未找到分离后的伴奏文件。")

            self._log("🔗 步骤 2/2: 合成双音轨 MKV (原声/伴奏)...")
            target_dir = self.out_dir_var.get().strip() or self.out_dir_default
            os.makedirs(target_dir, exist_ok=True)
            out_mkv = Path(target_dir) / f"{audio_name}.mkv"

            self._log(f"📤 输出路径: {out_mkv}")

            
            cmd_ffmpeg = [
                self.ffmpeg_exe, "-y",
                "-i", self.selected_mp4,       # 输入0:原视频 (含视频+原音轨)
                "-i", str(accompaniment),      # 输入1:AI分离伴奏 WAV

                # 1. 视频流(无损拷贝)
                "-map", "0:v:0", "-c:v", "copy",

                # 2. 音轨0:原视频混音音轨(设为默认播放)
                "-map", "0:a", "-c:a:0", "aac", "-b:a:0", "192k",
                "-metadata:s:a:0", "title=Original",
                "-disposition:a:0", "default",

                # 3. 音轨1:分离伴奏音轨
                "-map", "1:a", "-c:a:1", "aac", "-b:a:1", "192k",
                "-metadata:s:a:1", "title=Accompaniment",
                "-disposition:a:1", "0",

                str(out_mkv)
            ]

            subprocess.run(cmd_ffmpeg, capture_output=True, text=True, check=True, encoding="utf-8", errors="ignore")

            self.task_queue.put(("progress_stop", 100))
            self._log("🎉 处理完成!格式已严格匹配目标 MKV 规范。")
            self.task_queue.put(
                ("done", f"✅ 成功!\n双音轨 MKV 文件已保存至:\n{out_mkv}\n\n💡 播放器中切换「音轨」可对比原声与伴奏效果。"))

        except subprocess.CalledProcessError as e:
            full_err = (e.stderr or "") + (e.stdout or "")
            self._log(f"❌ 失败详情: {full_err[:200]}")
            self.task_queue.put(("error", f"处理失败:\n{full_err[:200]}"))
        except Exception as e:
            self._log(f"❌ 异常: {str(e)}")
            self.task_queue.put(("error", f"发生错误:\n{str(e)}"))
        finally:
            if os.path.exists(self.temp_dir):
                try:
                    shutil.rmtree(self.temp_dir)
                except:
                    pass
            self._log("🧹 临时文件已清理")


if __name__ == "__main__":
    root = tk.Tk()
    try:
        from ctypes import windll

        windll.shcore.SetProcessDpiAwareness(1)
    except:
        pass
    app = DualAudioApp(root)
    root.mainloop()

免费评分

参与人数 10吾爱币 +9 热心值 +9 收起 理由
ClenchSword + 1 + 1 贴主能给个exe吗?
abao117 + 1 + 1 加油
kexue8 + 1 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
weidechan + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
chinalaodeng + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
heavenman + 1 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
helian147 + 1 + 1 谢谢@Thanks!
szddsxj + 1 + 1 谢谢@Thanks!
bssqcdf + 1 + 1 谢谢@Thanks!
52kail + 1 谢谢@Thanks!

查看全部评分

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

沙发
52kail 发表于 2026-4-28 06:44
让我来学习学习,感谢分享!
3#
tingfengkanhai 发表于 2026-4-28 06:55
我一直有个疑问求解答,这种Python代码,怎么打包成exe和APK。
代码里边用到的插件,是要一起打包到成品里吗,不然怎么保证成品谁都可以用
4#
tzblue 发表于 2026-4-28 06:58
5#
52soft 发表于 2026-4-28 07:59
效果怎样?
6#
anzong5211314 发表于 2026-4-28 08:18
谢谢分享,下载试试
7#
qugengshun 发表于 2026-4-28 08:19
音视频编辑的好工具,先点赞后收藏!
8#
qugengshun 发表于 2026-4-28 08:20
真心希望楼主能造福坛友出一个成品,感谢付出!
9#
 楼主| psqladm 发表于 2026-4-28 08:24 |楼主

视频和混音质量依赖原MP4,这个没动。
音频分离,依赖大模型,大模型分标准版和增强版,文中用的是标准版,体积较小,速度较快,基本够用了。
如果追求音频分离的质量,可以换成增强版大模型,体积比较大,运行耗时要多几倍。
10#
fdiquan 发表于 2026-4-28 08:27
大佬有成品 的吗分享一下谢谢,对小白不会代码的怎么办
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

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

GMT+8, 2026-4-29 10:20

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

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