音频转换合并切割工具

先展示一下界面
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 struct class AudioMasterProX: def __init__(self, root): self.root = root self.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 = btn btn_frame.key = key return btn_frame def 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_frame def 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 = path self.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 = path self.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_files self.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("提示", "所选文件夹中没有找到音频文件!") return def start_convert(self): fmt = self.convert_fmt.get() if not hasattr(self, "convert_target"): messagebox.showwarning("提示", "请先选择音频文件或文件夹!") return self.progress_label.config(text="正在转换...", fg='#3182ce') self.progress['value'] = 0 if 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 = 0 failed_count = 0 for i, file_path in enumerate(file_list): try: # 更新进度 progress_percent = (i / total_files) * 100 self.progress['value'] = progress_percent self.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 += 1 filename = os.path.basename(file_path) self.log(f"✅ 转换完成: {filename}") else: failed_count += 1 filename = os.path.basename(file_path) self.log(f"❌ 转换失败: {filename}") except Exception as e: failed_count += 1 filename = os.path.basename(file_path) self.log(f"❌ 转换错误: {filename} - {e}") # 完成所有转换 self.progress['value'] = 100 self.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: return if 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 time temp_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') # 清除旧的canvas for widget in self.canvas_frame.winfo_children(): widget.destroy() # 创建新的canvas canvas = 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'] = 0 threading.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 time temp_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 time time.sleep(10) # 等待10秒后删除 try: os.remove(temp_preview) except: pass threading.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编码执行FFmpeg process = 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'] = 50 if 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: continue process.wait() # 检查执行结果 if process.returncode == 0: # 成功 if hasattr(self, 'cut_progress'): self.cut_progress['value'] = 100 if 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 = out threading.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编码执行FFmpeg process = 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'] = 50 if 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: continue process.wait() # 检查执行结果 if process.returncode == 0: # 成功 if hasattr(self, 'merge_progress'): self.merge_progress['value'] = 100 if 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编码执行FFmpeg process = 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'] = 50 if 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: # 如果解码失败,跳过这一行 continue process.wait() # 检查执行结果 if process.returncode == 0: # 成功 if hasattr(self, 'progress'): self.progress['value'] = 100 if 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()

浙公网安备 33010602011771号