ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

音频转换合并切割工具

音频转换合并切割工具

image

先展示一下界面

import os
import threading
import subprocess
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import ttkbootstrap as tb
from ttkbootstrap.constants import *
from tkinterdnd2 import DND_FILES, TkinterDnD
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np
import csv
import wave
import structclass AudioMasterProX:def __init__(self, root):self.root = rootself.root.title("AudioMaster Pro X - 三松集团专业音频处理工具 (强哥出品)")self.root.geometry("1400x900")self.root.configure(bg='#f5f7fa')# 使用专业的商业主题self.style = tb.Style("litera")# 设置窗口最小尺寸self.root.minsize(1200, 800)# 创建主容器 - 使用卡片式设计self.main_container = tk.Frame(self.root, bg='#f5f7fa')self.main_container.pack(fill="both", expand=True, padx=25, pady=25)# 创建应用头部
        self.create_app_header()# 创建导航和内容区域
        self.create_main_content()# 创建状态栏
        self.create_status_bar()# 初始化完成后,默认显示转换面板self.show_content_panel('convert')# 拖拽支持
        self.root.drop_target_register(DND_FILES)self.root.dnd_bind("<<Drop>>", self.handle_drop)self.log("🚀 AudioMaster Pro X 启动成功 - 专业音频处理,让工作更高效!")def create_app_header(self):"""创建应用头部 - 专业品牌设计"""# 头部容器header_container = tk.Frame(self.main_container, bg='#ffffff', relief='flat', bd=0)header_container.pack(fill="x", pady=(0, 20))# 添加微妙的阴影效果shadow_frame = tk.Frame(self.main_container, bg='#e8ecf0', height=3)shadow_frame.pack(fill="x", pady=(0, 17))# 头部内容header_content = tk.Frame(header_container, bg='#ffffff')header_content.pack(fill="x", padx=30, pady=25)# 左侧品牌区域brand_frame = tk.Frame(header_content, bg='#ffffff')brand_frame.pack(side="left")# 应用图标和标题icon_label = tk.Label(brand_frame, text="🎵", font=("Segoe UI Emoji", 32),bg='#ffffff',fg='#2c5aa0')icon_label.pack(side="left", padx=(0, 15))title_frame = tk.Frame(brand_frame, bg='#ffffff')title_frame.pack(side="left")# 主标题title_label = tk.Label(title_frame, text="AudioMaster Pro X", font=("Segoe UI", 28, "bold"),fg='#1a365d', bg='#ffffff')title_label.pack(anchor="w")# 副标题subtitle_label = tk.Label(title_frame,text="三松集团专业音频处理工具 | 强哥出品",font=("Segoe UI", 12),fg='#718096',bg='#ffffff')subtitle_label.pack(anchor="w", pady=(2, 0))# 右侧状态指示器status_frame = tk.Frame(header_content, bg='#ffffff')status_frame.pack(side="right")# 状态指示status_indicator = tk.Label(status_frame,text="● 就绪",font=("Segoe UI", 11),fg='#38a169',bg='#ffffff')status_indicator.pack(anchor="e")def create_main_content(self):"""创建主要内容区域 - 现代化卡片布局"""# 内容容器 - 减少底部间距content_container = tk.Frame(self.main_container, bg='#f5f7fa')content_container.pack(fill="both", expand=True, pady=(0, 5))# 左侧导航面板
        self.create_navigation_panel(content_container)# 右侧内容面板
        self.create_content_panel(content_container)def create_navigation_panel(self, parent):"""创建左侧导航面板"""nav_frame = tk.Frame(parent, bg='#ffffff', relief='flat', bd=0)nav_frame.pack(side="left", fill="y", padx=(0, 15))nav_frame.configure(width=280)nav_frame.pack_propagate(False)# 导航标题nav_title = tk.Label(nav_frame,text="功能导航",font=("Segoe UI", 14, "bold"),fg='#2d3748',bg='#ffffff')nav_title.pack(pady=(25, 15), padx=25, anchor="w")# 导航按钮self.nav_buttons = {}# 转换按钮self.nav_buttons['convert'] = self.create_nav_button(nav_frame, "🎧 音频格式转换", "高质量格式转换", "convert")# 切割按钮self.nav_buttons['cut'] = self.create_nav_button(nav_frame, "✂️ 音频切割", "精确时间切割", "cut")# 合成按钮self.nav_buttons['merge'] = self.create_nav_button(nav_frame, "🔗 音频合成", "多文件合并", "merge")# 注意:不在这里调用select_nav_button,因为content_frame还没有创建def create_nav_button(self, parent, title, desc, key):"""创建导航按钮"""btn_frame = tk.Frame(parent, bg='#ffffff', relief='flat', bd=0)btn_frame.pack(fill="x", padx=15, pady=5)# 按钮主体btn = tk.Button(btn_frame,text=f"{title}\n{desc}",font=("Segoe UI", 11),bg='#f7fafc',fg='#4a5568',relief='flat',bd=0,padx=20,pady=15,cursor='hand2',anchor='w',command=lambda: self.select_nav_button(key))btn.pack(fill="x")# 存储按钮引用btn_frame.btn = btnbtn_frame.key = keyreturn btn_framedef select_nav_button(self, key):"""选择导航按钮"""# 重置所有按钮样式for btn_frame in self.nav_buttons.values():btn_frame.btn.configure(bg='#f7fafc', fg='#4a5568')# 设置选中按钮样式if key in self.nav_buttons:self.nav_buttons[key].btn.configure(bg='#2c5aa0', fg='#ffffff')# 切换内容面板
        self.show_content_panel(key)def create_content_panel(self, parent):"""创建右侧内容面板"""self.content_frame = tk.Frame(parent, bg='#ffffff', relief='flat', bd=0)self.content_frame.pack(side="right", fill="both", expand=True)# 创建各个功能面板
        self.create_convert_panel()self.create_cut_panel()self.create_merge_panel()def show_content_panel(self, key):"""显示对应的内容面板"""# 隐藏所有面板for widget in self.content_frame.winfo_children():widget.pack_forget()# 显示选中的面板if key == 'convert':self.convert_panel.pack(fill="both", expand=True)elif key == 'cut':self.cut_panel.pack(fill="both", expand=True)elif key == 'merge':self.merge_panel.pack(fill="both", expand=True)def create_convert_panel(self):"""创建转换面板"""self.convert_panel = tk.Frame(self.content_frame, bg='#ffffff')# 面板标题 - 减少间距title_frame = tk.Frame(self.convert_panel, bg='#ffffff')title_frame.pack(fill="x", padx=20, pady=(15, 10))title_label = tk.Label(title_frame,text="🎧 音频格式转换",font=("Segoe UI", 20, "bold"),fg='#2d3748',bg='#ffffff')title_label.pack(anchor="w")desc_label = tk.Label(title_frame,text="支持多种音频格式之间的高质量转换,保持原始音质",font=("Segoe UI", 12),fg='#718096',bg='#ffffff')desc_label.pack(anchor="w", pady=(5, 0))# 主要内容区域 - 使用更紧凑的布局main_content = tk.Frame(self.convert_panel, bg='#ffffff')main_content.pack(fill="both", expand=True, padx=15, pady=(0, 10))# 创建三列布局以更好地利用空间left_column = tk.Frame(main_content, bg='#ffffff')left_column.pack(side="left", fill="both", expand=True, padx=(0, 8))center_column = tk.Frame(main_content, bg='#ffffff')center_column.pack(side="left", fill="both", expand=True, padx=(0, 8))right_column = tk.Frame(main_content, bg='#ffffff')right_column.pack(side="right", fill="both", expand=True, padx=(8, 0))# 文件选择卡片 - 更紧凑file_card = self.create_card(left_column, "📁 文件选择")# 文件选择按钮self.select_file_btn = tk.Button(file_card,text="📂 选择文件/文件夹",font=("Segoe UI", 10, "bold"),bg='#3182ce',fg='#ffffff',relief='flat',bd=0,padx=15,pady=8,cursor='hand2',command=self.select_convert_target)self.select_file_btn.pack(pady=(15, 8))# 文件信息显示self.file_info_label = tk.Label(file_card,text="未选择文件",font=("Segoe UI", 9),fg='#a0aec0',bg='#ffffff',wraplength=150)self.file_info_label.pack(pady=(0, 15))# 格式设置卡片 - 更紧凑format_card = self.create_card(center_column, "⚙️ 格式设置")# 格式选择format_label = tk.Label(format_card,text="目标格式:",font=("Segoe UI", 10),fg='#4a5568',bg='#ffffff')format_label.pack(anchor="w", padx=15, pady=(15, 5))self.convert_fmt = tk.StringVar(value="mp3")format_combo = ttk.Combobox(format_card,textvariable=self.convert_fmt,values=["mp3", "wav", "aac", "flac", "ogg"],font=("Segoe UI", 10),state="readonly",width=12)format_combo.pack(anchor="w", padx=15, pady=(0, 10))# 转换按钮convert_btn = tk.Button(format_card,text="🚀 开始转换",font=("Segoe UI", 10, "bold"),bg='#38a169',fg='#ffffff',relief='flat',bd=0,padx=15,pady=8,cursor='hand2',command=self.start_convert)convert_btn.pack(pady=(0, 15))# 进度卡片 - 更紧凑progress_card = self.create_card(right_column, "⏳ 转换进度")# 进度标签self.progress_label = tk.Label(progress_card,text="等待开始...",font=("Segoe UI", 10),fg='#4a5568',bg='#ffffff')self.progress_label.pack(anchor="w", padx=15, pady=(15, 5))# 进度条self.progress = ttk.Progressbar(progress_card,mode="determinate",length=200,style="Custom.Horizontal.TProgressbar")self.progress.pack(fill="x", padx=15, pady=(0, 15))# 添加转换信息卡片info_card = self.create_card(right_column, "ℹ️ 转换信息")info_text = tk.Text(info_card,height=6,bg='#f7fafc',fg='#2d3748',font=("Segoe UI", 8),relief='flat',bd=1,wrap='word')info_text.pack(fill="both", expand=True, padx=15, pady=15)# 添加有用的信息info_content = """支持的格式:
• MP3 - 通用压缩
• WAV - 无损音频
• AAC - 高质量压缩
• FLAC - 无损压缩
• OGG - 开源格式特点:
• 保持原始音质
• 支持批量转换
• 快速处理速度"""info_text.insert(tk.END, info_content)info_text.config(state='disabled')def create_card(self, parent, title):"""创建卡片组件"""card_frame = tk.LabelFrame(parent,text=title,font=("Segoe UI", 12, "bold"),fg='#2d3748',bg='#ffffff',relief='flat',bd=1)card_frame.pack(fill="x", pady=(0, 10))return card_framedef create_cut_panel(self):"""创建切割面板"""self.cut_panel = tk.Frame(self.content_frame, bg='#ffffff')# 面板标题 - 减少间距title_frame = tk.Frame(self.cut_panel, bg='#ffffff')title_frame.pack(fill="x", padx=20, pady=(15, 10))title_label = tk.Label(title_frame,text="✂️ 音频切割",font=("Segoe UI", 20, "bold"),fg='#2d3748',bg='#ffffff')title_label.pack(anchor="w")desc_label = tk.Label(title_frame,text="精确切割音频片段,支持批量处理和时间范围选择",font=("Segoe UI", 12),fg='#718096',bg='#ffffff')desc_label.pack(anchor="w", pady=(5, 0))# 主要内容区域 - 减少间距main_content = tk.Frame(self.cut_panel, bg='#ffffff')main_content.pack(fill="both", expand=True, padx=15, pady=(0, 10))# 左侧控制区域left_section = tk.Frame(main_content, bg='#ffffff')left_section.pack(side="left", fill="both", expand=True, padx=(0, 15))# 文件选择卡片file_card = self.create_card(left_section, "📁 选择音频文件")self.cut_select_btn = tk.Button(file_card,text="📂 选择音频文件",font=("Segoe UI", 12),bg='#3182ce',fg='#ffffff',relief='flat',bd=0,padx=25,pady=12,cursor='hand2',command=self.select_cut_file)self.cut_select_btn.pack(pady=20)self.cut_file_info_label = tk.Label(file_card,text="未选择文件",font=("Segoe UI", 11),fg='#a0aec0',bg='#ffffff',wraplength=300)self.cut_file_info_label.pack(pady=(0, 20))# 时间设置卡片time_card = self.create_card(left_section, "⏰ 时间设置")# 开始时间start_label = tk.Label(time_card,text="开始时间 (HH:MM:SS):",font=("Segoe UI", 12),fg='#4a5568',bg='#ffffff')start_label.pack(anchor="w", padx=20, pady=(20, 5))self.start_var = tk.StringVar(value="00:00:00")start_entry = tk.Entry(time_card,textvariable=self.start_var,font=("Segoe UI", 11),width=20,relief='flat',bd=1)start_entry.pack(anchor="w", padx=20, pady=(0, 15))# 结束时间end_label = tk.Label(time_card,text="结束时间 (HH:MM:SS):",font=("Segoe UI", 12),fg='#4a5568',bg='#ffffff')end_label.pack(anchor="w", padx=20, pady=(10, 5))self.end_var = tk.StringVar(value="00:00:10")end_entry = tk.Entry(time_card,textvariable=self.end_var,font=("Segoe UI", 11),width=20,relief='flat',bd=1)end_entry.pack(anchor="w", padx=20, pady=(0, 20))# 操作按钮区域button_frame = tk.Frame(time_card, bg='#ffffff')button_frame.pack(fill="x", padx=20, pady=(0, 20))# 执行切割按钮cut_btn = tk.Button(button_frame,text="✂️ 执行切割",font=("Segoe UI", 11, "bold"),bg='#e53e3e',fg='#ffffff',relief='flat',bd=0,padx=20,pady=10,cursor='hand2',command=self.cut_audio)cut_btn.pack(side="left", padx=(0, 10))# 播放预览按钮preview_btn = tk.Button(button_frame,text="▶️ 播放预览",font=("Segoe UI", 11, "bold"),bg='#d69e2e',fg='#ffffff',relief='flat',bd=0,padx=20,pady=10,cursor='hand2',command=self.preview_cut_audio)preview_btn.pack(side="left", padx=(0, 10))# CSV批量切割按钮batch_btn = tk.Button(button_frame,text="📋 导入CSV批量切割",font=("Segoe UI", 11),bg='#805ad5',fg='#ffffff',relief='flat',bd=0,padx=20,pady=10,cursor='hand2',command=self.load_csv_cut)batch_btn.pack(side="left")# 右侧波形区域right_section = tk.Frame(main_content, bg='#ffffff')right_section.pack(side="right", fill="both", expand=True, padx=(15, 0))# 波形预览卡片waveform_card = self.create_card(right_section, "📊 波形预览")self.canvas_frame = tk.Frame(waveform_card, bg='#ffffff')self.canvas_frame.pack(fill="both", expand=True, padx=20, pady=20)# 切割进度卡片progress_card = self.create_card(right_section, "⏳ 切割进度")# 进度标签self.cut_progress_label = tk.Label(progress_card,text="等待开始...",font=("Segoe UI", 11),fg='#4a5568',bg='#ffffff')self.cut_progress_label.pack(anchor="w", padx=20, pady=(15, 5))# 进度条self.cut_progress = ttk.Progressbar(progress_card,mode='determinate',length=300)self.cut_progress.pack(fill="x", padx=20, pady=(0, 15))def create_merge_panel(self):"""创建合成面板"""self.merge_panel = tk.Frame(self.content_frame, bg='#ffffff')# 面板标题 - 减少间距title_frame = tk.Frame(self.merge_panel, bg='#ffffff')title_frame.pack(fill="x", padx=20, pady=(15, 10))title_label = tk.Label(title_frame,text="🔗 音频合成",font=("Segoe UI", 20, "bold"),fg='#2d3748',bg='#ffffff')title_label.pack(anchor="w")desc_label = tk.Label(title_frame,text="将多个音频文件合并为一个完整文件,支持不同格式混合",font=("Segoe UI", 12),fg='#718096',bg='#ffffff')desc_label.pack(anchor="w", pady=(5, 0))# 主要内容区域 - 使用更紧凑的布局main_content = tk.Frame(self.merge_panel, bg='#ffffff')main_content.pack(fill="both", expand=True, padx=15, pady=(0, 10))# 左侧文件管理区域left_section = tk.Frame(main_content, bg='#ffffff')left_section.pack(side="left", fill="both", expand=True, padx=(0, 15))# 文件选择卡片file_card = self.create_card(left_section, "📁 选择音频文件")self.merge_select_btn = tk.Button(file_card,text="📂 选择多个文件",font=("Segoe UI", 12),bg='#3182ce',fg='#ffffff',relief='flat',bd=0,padx=25,pady=12,cursor='hand2',command=self.select_merge_files)self.merge_select_btn.pack(pady=20)# 文件列表显示list_frame = tk.Frame(file_card, bg='#ffffff')list_frame.pack(fill="both", expand=True, padx=20, pady=(0, 20))self.file_list_text = tk.Text(list_frame,height=8,bg='#f7fafc',fg='#2d3748',font=("Segoe UI", 10),relief='flat',bd=1,wrap='word')self.file_list_text.pack(side="left", fill="both", expand=True)scrollbar = ttk.Scrollbar(list_frame, orient="vertical", command=self.file_list_text.yview)scrollbar.pack(side="right", fill="y")self.file_list_text.configure(yscrollcommand=scrollbar.set)# 右侧控制区域right_section = tk.Frame(main_content, bg='#ffffff')right_section.pack(side="right", fill="both", expand=True, padx=(15, 0))# 合成设置卡片merge_card = self.create_card(right_section, "⚙️ 合成设置")info_label = tk.Label(merge_card,text="文件将按照选择顺序进行合成",font=("Segoe UI", 11),fg='#718096',bg='#ffffff',wraplength=250)info_label.pack(padx=20, pady=(20, 15))merge_btn = tk.Button(merge_card,text="🔗 开始合成",font=("Segoe UI", 12, "bold"),bg='#d69e2e',fg='#ffffff',relief='flat',bd=0,padx=30,pady=12,cursor='hand2',command=self.merge_audio)merge_btn.pack(pady=(0, 15))# 合成进度self.merge_progress_label = tk.Label(merge_card,text="等待开始...",font=("Segoe UI", 11),fg='#4a5568',bg='#ffffff')self.merge_progress_label.pack(anchor="w", padx=20, pady=(0, 5))self.merge_progress = ttk.Progressbar(merge_card,mode='determinate',length=250)self.merge_progress.pack(fill="x", padx=20, pady=(0, 15))# 合并结果预览区域result_frame = tk.Frame(merge_card, bg='#ffffff')result_frame.pack(fill="x", padx=20, pady=(0, 15))result_label = tk.Label(result_frame,text="合并结果:",font=("Segoe UI", 11, "bold"),fg='#2d3748',bg='#ffffff')result_label.pack(anchor="w", pady=(0, 5))self.result_file_label = tk.Label(result_frame,text="暂无文件",font=("Segoe UI", 10),fg='#718096',bg='#ffffff',wraplength=200)self.result_file_label.pack(anchor="w", pady=(0, 10))# 预览和播放按钮preview_frame = tk.Frame(result_frame, bg='#ffffff')preview_frame.pack(fill="x")self.preview_result_btn = tk.Button(preview_frame,text="👁️ 预览",font=("Segoe UI", 10),bg='#3182ce',fg='#ffffff',relief='flat',bd=0,padx=15,pady=8,cursor='hand2',command=self.preview_merged_audio,state='disabled')self.preview_result_btn.pack(side="left", padx=(0, 10))self.play_result_btn = tk.Button(preview_frame,text="▶️ 播放",font=("Segoe UI", 10),bg='#38a169',fg='#ffffff',relief='flat',bd=0,padx=15,pady=8,cursor='hand2',command=self.play_merged_audio,state='disabled')self.play_result_btn.pack(side="left")# 文件统计卡片stats_card = self.create_card(right_section, "📊 文件统计")self.stats_label = tk.Label(stats_card,text="未选择文件",font=("Segoe UI", 11),fg='#a0aec0',bg='#ffffff')self.stats_label.pack(padx=20, pady=20)self.merge_list = []def create_status_bar(self):"""创建状态栏"""status_container = tk.Frame(self.main_container, bg='#ffffff', relief='flat', bd=0)status_container.pack(fill="x", pady=(5, 0))# 添加微妙的阴影效果shadow_frame = tk.Frame(self.main_container, bg='#e8ecf0', height=3)shadow_frame.pack(fill="x", pady=(17, 0))# 状态栏内容 - 减少间距status_content = tk.Frame(status_container, bg='#ffffff')status_content.pack(fill="x", padx=20, pady=8)# 左侧日志标题log_title = tk.Label(status_content,text="📋 操作日志",font=("Segoe UI", 12, "bold"),fg='#2d3748',bg='#ffffff')log_title.pack(side="left")# 右侧状态信息status_info = tk.Label(status_content,text="● 系统就绪",font=("Segoe UI", 11),fg='#38a169',bg='#ffffff')status_info.pack(side="right")# 日志文本框 - 更紧凑log_frame = tk.Frame(status_container, bg='#ffffff')log_frame.pack(fill="x", padx=20, pady=(0, 5))self.log_text = tk.Text(log_frame, height=4,  # 减少高度bg='#f7fafc', fg='#2d3748',font=("Consolas", 9),  # 减小字体relief='flat',bd=1,wrap='word')self.log_text.pack(side="left", fill="both", expand=True)# 滚动条scrollbar = ttk.Scrollbar(log_frame, orient="vertical", command=self.log_text.yview)scrollbar.pack(side="right", fill="y")self.log_text.configure(yscrollcommand=scrollbar.set)# ----------------- 基础日志功能 -----------------def log(self, msg):self.log_text.insert("end", msg + "\n")self.log_text.see("end")def handle_drop(self, event):try:paths = self.root.tk.splitlist(event.data)for path in paths:self.log(f"🟢 拖入文件: {path}")if path.lower().endswith((".mp3", ".wav", ".aac", ".flac", ".ogg")):self.last_dropped = pathself.display_waveform(path)except Exception as e:self.log(f"❌ 拖拽处理错误: {e}")# ----------------- 转换模块 -----------------def select_convert_target(self):# 创建选择对话框choice = messagebox.askyesnocancel("选择类型", "选择文件类型:\n\n是 - 选择单个文件\n否 - 选择文件夹(批量转换)\n取消 - 取消操作")if choice is True:  # 选择单个文件path = filedialog.askopenfilename(filetypes=[("音频文件", "*.wav *.mp3 *.aac *.flac *.ogg")])if path:self.convert_target = pathself.convert_type = "file"filename = os.path.basename(path)self.file_info_label.config(text=f"已选择文件: {filename}", fg='#38a169')self.log(f"🎵 已选择音频文件: {filename}")elif choice is False:  # 选择文件夹folder_path = filedialog.askdirectory(title="选择包含音频文件的文件夹")if folder_path:# 扫描文件夹中的音频文件audio_files = []audio_extensions = ['.wav', '.mp3', '.aac', '.flac', '.ogg', '.m4a', '.wma']for root, dirs, files in os.walk(folder_path):for file in files:if any(file.lower().endswith(ext) for ext in audio_extensions):audio_files.append(os.path.join(root, file))if audio_files:self.convert_target = audio_filesself.convert_type = "folder"folder_name = os.path.basename(folder_path)self.file_info_label.config(text=f"已选择文件夹: {folder_name} ({len(audio_files)}个文件)", fg='#38a169')self.log(f"📁 已选择文件夹: {folder_name},包含 {len(audio_files)} 个音频文件")else:messagebox.showwarning("提示", "所选文件夹中没有找到音频文件!")returndef start_convert(self):fmt = self.convert_fmt.get()if not hasattr(self, "convert_target"):messagebox.showwarning("提示", "请先选择音频文件或文件夹!")returnself.progress_label.config(text="正在转换...", fg='#3182ce')self.progress['value'] = 0if hasattr(self, 'convert_type') and self.convert_type == "folder":# 批量转换文件夹中的文件
            self.batch_convert_files(self.convert_target, fmt)else:# 单个文件转换out = os.path.splitext(self.convert_target)[0] + f".{fmt}"cmd = f'ffmpeg -y -i "{self.convert_target}" -q:a 2 "{out}"'threading.Thread(target=lambda: self.run_ffmpeg(cmd, f"✅ 转换完成:{out}")).start()def batch_convert_files(self, file_list, fmt):"""批量转换文件"""total_files = len(file_list)self.log(f"🔄 开始批量转换 {total_files} 个文件...")def convert_worker():success_count = 0failed_count = 0for i, file_path in enumerate(file_list):try:# 更新进度progress_percent = (i / total_files) * 100self.progress['value'] = progress_percentself.progress_label.config(text=f"正在转换 {i+1}/{total_files}...", fg='#3182ce')# 生成输出文件名base_name = os.path.splitext(file_path)[0]out_path = f"{base_name}.{fmt}"# 执行转换cmd = f'ffmpeg -y -i "{file_path}" -q:a 2 "{out_path}"'# 设置环境变量确保正确的编码env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'process = subprocess.run(cmd, shell=True, capture_output=True, text=True,encoding='utf-8',errors='ignore',env=env)if process.returncode == 0:success_count += 1filename = os.path.basename(file_path)self.log(f"✅ 转换完成: {filename}")else:failed_count += 1filename = os.path.basename(file_path)self.log(f"❌ 转换失败: {filename}")except Exception as e:failed_count += 1filename = os.path.basename(file_path)self.log(f"❌ 转换错误: {filename} - {e}")# 完成所有转换self.progress['value'] = 100self.progress_label.config(text=f"批量转换完成! 成功: {success_count}, 失败: {failed_count}", fg='#38a169')self.log(f"🎉 批量转换完成! 成功: {success_count} 个文件, 失败: {failed_count} 个文件")# 在后台线程中执行批量转换threading.Thread(target=convert_worker, daemon=True).start()# ----------------- 音频切割模块 -----------------def select_cut_file(self):self.cut_file = filedialog.askopenfilename(filetypes=[("音频文件", "*.wav *.mp3 *.aac *.flac *.ogg")])if self.cut_file:filename = os.path.basename(self.cut_file)self.cut_file_info_label.config(text=f"已选择: {filename}", fg='#38a169')self.display_waveform(self.cut_file)self.log(f"🎵 已选择切割文件: {filename}")def load_csv_cut(self):csv_path = filedialog.askopenfilename(filetypes=[("CSV 文件", "*.csv")])if not csv_path: returnif not hasattr(self, "cut_file"):return messagebox.showwarning("提示", "请先选择音频文件!")try:with open(csv_path, newline='', encoding='utf-8') as f:reader = csv.reader(f)for row in reader:if len(row) >= 2:start, end = row[0], row[1]self.cut_audio_batch(start, end)except Exception as e:self.log(f"❌ CSV读取错误: {e}")def display_waveform(self, path):try:# 使用FFmpeg获取音频数据import timetemp_wav = f"temp_waveform_{int(time.time())}.wav"cmd = f'ffmpeg -y -i "{path}" -ac 1 -ar 8000 -f wav "{temp_wav}"'# 执行FFmpeg命令env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'process = subprocess.run(cmd, shell=True, capture_output=True, text=True,encoding='utf-8',errors='ignore',env=env)if os.path.exists(temp_wav):# 读取WAV文件with wave.open(temp_wav, 'rb') as wav_file:frames = wav_file.readframes(-1)sound_info = struct.unpack('h' * (len(frames) // 2), frames)# 降采样以提高性能step = max(1, len(sound_info) // 2000)  # 最多显示2000个点data = np.array(sound_info[::step])# 创建波形图fig, ax = plt.subplots(figsize=(8, 3), facecolor='white')ax.plot(data, linewidth=0.8, color='#3182ce')ax.fill_between(range(len(data)), data, alpha=0.3, color='#3182ce')ax.set_title("音频波形预览", fontsize=12, fontweight='bold', color='#2d3748')ax.set_xlabel("时间", fontsize=10, color='#718096')ax.set_ylabel("振幅", fontsize=10, color='#718096')ax.grid(True, alpha=0.3)ax.set_facecolor('#f7fafc')# 清除旧的canvasfor widget in self.canvas_frame.winfo_children(): widget.destroy()# 创建新的canvascanvas = FigureCanvasTkAgg(fig, master=self.canvas_frame)canvas.draw()canvas.get_tk_widget().pack(fill="both", expand=True)# 清理临时文件try:os.remove(temp_wav)except:pass  # 忽略删除失败
                    self.log(f"📊 波形显示成功: {os.path.basename(path)}")else:self.log(f"⚠️ 波形生成失败: {path}")except Exception as e:self.log(f"⚠️ 波形显示失败: {e}")# 如果波形显示失败,显示一个简单的文本提示for widget in self.canvas_frame.winfo_children(): widget.destroy()label = tk.Label(self.canvas_frame, text="波形预览不可用\n请确保FFmpeg已正确安装", font=("Segoe UI", 12), fg='#a0aec0',bg='#ffffff')label.pack(expand=True)def cut_audio(self):if not hasattr(self, "cut_file"):return messagebox.showwarning("提示", "请先选择音频文件!")start, end = self.start_var.get(), self.end_var.get()if not start or not end:return messagebox.showwarning("提示", "请输入开始和结束时间!")out = os.path.splitext(self.cut_file)[0] + f"_{start.replace(':','')}-{end.replace(':','')}.mp3"cmd = f'ffmpeg -y -i "{self.cut_file}" -ss {start} -to {end} -c copy "{out}"'# 更新切割进度if hasattr(self, 'cut_progress_label'):self.cut_progress_label.config(text="正在切割...", fg='#3182ce')if hasattr(self, 'cut_progress'):self.cut_progress['value'] = 0threading.Thread(target=lambda: self.run_cut_ffmpeg(cmd, f"✂️ 切割完成: {out}")).start()def preview_cut_audio(self):"""预览切割的音频片段"""if not hasattr(self, "cut_file"):return messagebox.showwarning("提示", "请先选择音频文件!")start, end = self.start_var.get(), self.end_var.get()if not start or not end:return messagebox.showwarning("提示", "请输入开始和结束时间!")try:# 创建临时预览文件import timetemp_preview = f"temp_preview_{int(time.time())}.mp3"cmd = f'ffmpeg -y -i "{self.cut_file}" -ss {start} -to {end} -c copy "{temp_preview}"'# 执行预览切割env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'process = subprocess.run(cmd, shell=True, capture_output=True, text=True,encoding='utf-8',errors='ignore',env=env)if process.returncode == 0 and os.path.exists(temp_preview):# 使用系统默认播放器播放
                os.startfile(temp_preview)self.log(f"🎵 正在播放预览: {start} - {end}")# 延迟删除临时文件def cleanup():import timetime.sleep(10)  # 等待10秒后删除try:os.remove(temp_preview)except:passthreading.Thread(target=cleanup, daemon=True).start()else:self.log(f"❌ 预览生成失败")except Exception as e:self.log(f"❌ 预览播放错误: {e}")def run_cut_ffmpeg(self, cmd, done_msg):"""专门用于切割的FFmpeg执行函数"""try:self.log(f"🔄 开始执行: {cmd.split()[-1]}")# 设置环境变量确保正确的编码env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'# 使用UTF-8编码执行FFmpegprocess = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True,encoding='utf-8',errors='ignore',env=env)# 更新进度条if hasattr(self, 'cut_progress'):self.cut_progress['value'] = 50if hasattr(self, 'cut_progress_label'):self.cut_progress_label.config(text="处理中...", fg='#3182ce')# 读取输出并处理output_lines = []for line in process.stdout:try:clean_line = line.strip()if clean_line:output_lines.append(clean_line)if "time=" in clean_line: self.log(clean_line)except UnicodeDecodeError:continueprocess.wait()# 检查执行结果if process.returncode == 0:# 成功if hasattr(self, 'cut_progress'):self.cut_progress['value'] = 100if hasattr(self, 'cut_progress_label'):self.cut_progress_label.config(text="完成!", fg='#38a169')self.log(done_msg)else:# 失败if hasattr(self, 'cut_progress_label'):self.cut_progress_label.config(text="失败", fg='#e53e3e')self.log(f"❌ FFmpeg 执行失败,返回码: {process.returncode}")for line in output_lines[-3:]:if line:self.log(f"   {line}")except Exception as e:self.log(f"❌ FFmpeg 执行错误: {e}")if hasattr(self, 'cut_progress_label'):self.cut_progress_label.config(text="失败", fg='#e53e3e')def cut_audio_batch(self, start, end):out = os.path.splitext(self.cut_file)[0] + f"_{start.replace(':','')}-{end.replace(':','')}.mp3"cmd = f'ffmpeg -y -i "{self.cut_file}" -ss {start} -to {end} -c copy "{out}"'self.run_ffmpeg(cmd, f"✅ 批量切割完成: {out}")# ----------------- 音频合成模块 -----------------def select_merge_files(self):files = filedialog.askopenfilenames(filetypes=[("音频文件", "*.wav *.mp3 *.aac *.flac *.ogg")])self.merge_list = list(files)# 更新文件列表显示self.file_list_text.delete(1.0, tk.END)if files:for i, f in enumerate(files, 1):filename = os.path.basename(f)self.file_list_text.insert(tk.END, f"{i}. {filename}\n")# 更新统计信息self.stats_label.config(text=f"已选择 {len(files)} 个文件", fg='#38a169')for f in files:filename = os.path.basename(f)self.log(f"📎 添加合成文件: {filename}")else:self.stats_label.config(text="未选择文件", fg='#a0aec0')def merge_audio(self):if not self.merge_list:return messagebox.showwarning("提示", "请先选择音频文件!")try:with open("merge_list.txt", "w", encoding="utf-8") as f:for path in self.merge_list:f.write(f"file '{path.replace('\'', '\\\'')}'\n")out = os.path.join(os.path.dirname(self.merge_list[0]), "merged_output.mp3")cmd = f'ffmpeg -y -f concat -safe 0 -i merge_list.txt -c copy "{out}"'# 更新合成进度if hasattr(self, 'merge_progress_label'):self.merge_progress_label.config(text="正在合成...", fg='#3182ce')if hasattr(self, 'merge_progress'):self.merge_progress['value'] = 0# 存储输出文件路径用于后续预览self.last_merged_file = outthreading.Thread(target=lambda: self.run_merge_ffmpeg(cmd, f"🔗 合成完成: {out}")).start()except Exception as e:self.log(f"❌ 合成准备错误: {e}")def run_merge_ffmpeg(self, cmd, done_msg):"""专门用于合成的FFmpeg执行函数"""try:self.log(f"🔄 开始执行: {cmd.split()[-1]}")# 设置环境变量确保正确的编码env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'# 使用UTF-8编码执行FFmpegprocess = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True,encoding='utf-8',errors='ignore',env=env)# 更新进度条if hasattr(self, 'merge_progress'):self.merge_progress['value'] = 50if hasattr(self, 'merge_progress_label'):self.merge_progress_label.config(text="处理中...", fg='#3182ce')# 读取输出并处理output_lines = []for line in process.stdout:try:clean_line = line.strip()if clean_line:output_lines.append(clean_line)if "time=" in clean_line: self.log(clean_line)except UnicodeDecodeError:continueprocess.wait()# 检查执行结果if process.returncode == 0:# 成功if hasattr(self, 'merge_progress'):self.merge_progress['value'] = 100if hasattr(self, 'merge_progress_label'):self.merge_progress_label.config(text="完成!", fg='#38a169')# 更新结果文件显示和按钮状态if hasattr(self, 'result_file_label'):self.result_file_label.config(text=os.path.basename(self.last_merged_file), fg='#2d3748')if hasattr(self, 'preview_result_btn'):self.preview_result_btn.config(state='normal')if hasattr(self, 'play_result_btn'):self.play_result_btn.config(state='normal')self.log(done_msg)else:# 失败if hasattr(self, 'merge_progress_label'):self.merge_progress_label.config(text="失败", fg='#e53e3e')self.log(f"❌ FFmpeg 执行失败,返回码: {process.returncode}")for line in output_lines[-3:]:if line:self.log(f"   {line}")except Exception as e:self.log(f"❌ FFmpeg 执行错误: {e}")if hasattr(self, 'merge_progress_label'):self.merge_progress_label.config(text="失败", fg='#e53e3e')def preview_merged_audio(self):"""预览合并后的音频文件"""if hasattr(self, 'last_merged_file') and os.path.exists(self.last_merged_file):try:# 使用系统默认播放器播放
                os.startfile(self.last_merged_file)self.log(f"🎵 正在播放合并结果: {os.path.basename(self.last_merged_file)}")except Exception as e:self.log(f"❌ 预览播放错误: {e}")else:self.log("❌ 没有可预览的合并文件")def play_merged_audio(self):"""播放合并后的音频文件"""if hasattr(self, 'last_merged_file') and os.path.exists(self.last_merged_file):try:# 使用系统默认播放器播放
                os.startfile(self.last_merged_file)self.log(f"🎵 正在播放合并结果: {os.path.basename(self.last_merged_file)}")except Exception as e:self.log(f"❌ 播放错误: {e}")else:self.log("❌ 没有可播放的合并文件")# ----------------- FFmpeg 执行函数 -----------------def run_ffmpeg(self, cmd, done_msg):try:self.log(f"🔄 开始执行: {cmd.split()[-1]}")# 设置环境变量确保正确的编码env = os.environ.copy()env['PYTHONIOENCODING'] = 'utf-8'# 使用UTF-8编码执行FFmpegprocess = subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, universal_newlines=True,encoding='utf-8',errors='ignore',  # 忽略编码错误env=env)# 更新进度条if hasattr(self, 'progress'):self.progress['value'] = 50if hasattr(self, 'progress_label'):self.progress_label.config(text="处理中...", fg='#3182ce')# 读取输出并处理output_lines = []for line in process.stdout:try:# 尝试解码每一行clean_line = line.strip()if clean_line:output_lines.append(clean_line)if "time=" in clean_line: self.log(clean_line)except UnicodeDecodeError:# 如果解码失败,跳过这一行continueprocess.wait()# 检查执行结果if process.returncode == 0:# 成功if hasattr(self, 'progress'):self.progress['value'] = 100if hasattr(self, 'progress_label'):self.progress_label.config(text="完成!", fg='#38a169')self.log(done_msg)else:# 失败if hasattr(self, 'progress_label'):self.progress_label.config(text="失败", fg='#e53e3e')self.log(f"❌ FFmpeg 执行失败,返回码: {process.returncode}")# 显示最后几行错误信息for line in output_lines[-3:]:if line:self.log(f"   {line}")except Exception as e:self.log(f"❌ FFmpeg 执行错误: {e}")if hasattr(self, 'progress_label'):self.progress_label.config(text="失败", fg='#e53e3e')if __name__ == "__main__":root = TkinterDnD.Tk()app = AudioMasterProX(root)root.mainloop()

 

返回列表