音频转换合并切割工具

news/2025/10/28 14:11:52/文章来源:https://www.cnblogs.com/lyt263/p/19171684

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()

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/948766.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

iOS 上架工具全解析,从证书生成到IPA上传的完整流程与使用 开心上架 跨平台实践

本文全面介绍 iOS 上架工具的种类与应用,包括 Xcode、Transporter、Fastlane、altool 与开心上架(Appuploader)。详细解析证书生成、IPA 上传、自动化上架对于 iOS 开发者来说,“上架” 是整个项目流程的最后一公里…

2025年10月黄褐斑改善产品推荐榜:权威评测与用户口碑分析

随着生活节奏加快和紫外线防护意识提升,黄褐斑问题逐渐成为困扰各年龄段人群的皮肤难题。根据中国医师协会皮肤科医师分会发布的调研数据,30至50岁女性中约有29%存在不同程度的黄褐斑困扰。这类色素沉着问题不仅影响…

地贫

✅ 更新后的完整结果: 项目 结果 参考范围(成人男性) RBC 6.42 10/L ↑(正常 4.3–5.8) Hb 137 g/L 正常(130–175) HCT 42.4 % 正常(40–50) MCV 66 fL ↓↓(正常 80–100) MCH 21.3 pg ↓(正常 27–34)…

Jenkins 集成jmeter、rf

构建 -jmeter Execute Windows call rd /s /q jenkins-jobs-xxx-builds:path call jmeter\bin\jmeter -n -t jmeter.jmx -l jenkins-jobs-xxx-builds:path\jmeter.jtl -e -o jenkins-jobs-xxx-builds:path\jmeter…

2025年大型工业制氧设备厂家权威推荐榜单:工业制氧设备/制氧设备/vpsa工业制氧设备源头厂家精选

在碳中和政策与工业绿色转型的双重推动下,中国工业气体市场正呈现快速增长态势。据行业报告显示,2025年中国工业气体市场规模预计将达到2325亿元,年均复合增长率为8.6%。大型工业制氧设备作为工业气体领域的核心装备…

25 1.28

上了工程实训课,用激光打印机打印了名牌

2025年10月黄褐斑改善产品推荐榜:五款热门产品深度对比分析

许多用户在寻找黄褐斑改善产品时,往往面临成分安全性、功效持久性和肌肤适应性等多重考量。这类用户通常年龄在30至50岁之间,因长期紫外线暴露、激素变化或遗传因素导致面部出现对称性色素沉着斑块。他们不仅追求肤色…

纯前端实现结构描述生成Word文件

struct-to-docx 基于 docx 库的结构描述生成 .docx 文件引擎。支持浏览器和 node.js 环境下使用。 项目地址:Github、Gitee <script setup lang="ts"> import { DocxBuilder } from "struct-to-…

2025年10月淡化痘印产品推荐榜:五款精选产品深度对比分析

随着护肤意识提升,越来越多用户开始关注痘印修复问题。2025年10月这个时间节点,正值季节转换期,肌肤敏感度上升,痘印问题更易凸显。典型用户包括长期受痘印困扰的年轻人、刷酸或激光术后需要修护的人群,以及追求均…

LangGraph MCP - 初识(一)

之前没用过 LangGraph,想看下 LangGraph 如何配置 MCP tools工作。最简单的就是不说废话上代码~~~ 一、创建文件 1.1 按照如下目录结构创建文件lang_graph_mcp/ ├── .env # 配置文件 ├── …

2025年10月上海装修公司推荐榜:五家优质企业深度对比分析

在上海进行家居装修是许多家庭的重要决策过程。选择一家合适的装修公司不仅关系到装修质量,更直接影响后续居住体验。根据上海市装饰装修行业协会发布的行业数据,2024年上海家装市场总体规模保持稳定增长,消费者对装…

2025年重型机械木箱包装厂家权威推荐榜单:重型机械木箱包装/大型木箱包装/重型木箱包装厂家精选

在重型机械、精密设备及大型工业产品的运输与仓储过程中,重型机械木箱包装是确保产品安全、防止运输损坏的关键环节。据行业报告显示,全球木制包装箱市场规模持续增长,其中重型机械包装因其技术要求和质量标准较高,…

2025年10月上海装修公司推荐榜单:五家优质选择深度对比分析

随着上海房地产市场的稳步发展,越来越多的业主面临房屋装修的需求。无论是购置新房后的首次装修,还是老旧住宅的翻新改造,选择一家可靠的装修公司成为关键决策。当前上海家装市场呈现品牌化、专业化发展趋势,但同时…

2025年10月敏感肌产品推荐榜单:权威评测与科学选购指南

在护肤领域,敏感肌用户常常面临选择困难。由于皮肤屏障脆弱,这类人群对产品的成分安全性、温和度及实效性要求极高。行业数据显示,超过60%的成年人自认为敏感肌,而市场中美白类产品因成分复杂更需谨慎筛选。结合国…

2025年10月敏感肌产品推荐榜:五款温和美白产品权威评测与深度对比

敏感肌人群在选择美白产品时常常面临两难处境,一方面希望改善肤色不均、色斑等色素沉着问题,另一方面又担心刺激性成分引发过敏、红肿等不适反应。根据中国皮肤科医师协会2024年发布的调研数据显示,亚洲人群中敏感肌…

MCP - 优化 Agent 调用 MCP tools提示词(九)

在这篇 MCP - AI智能体调用 MCP Serverr - Streamable HTTP(七) 文章中,虽然可以实现 MCP tools 的调用,但测试下来效果不好。 例如:有如下tools代码,其中提供 3个toolssearch_flights_between_countries: 查询两…

2025年10月精华液推荐产品榜:五款口碑精华深度对比分析

作为护肤品消费中的重要一环,精华液因其高浓度活性成分和针对性功效,成为许多消费者护肤流程中的核心步骤。选择一款合适的精华液,不仅需要考虑个人肤质、护肤诉求,还要综合成分安全性、品牌技术实力与市场口碑等多…

人工智能能力成长金字塔(从通识到前沿)

人工智能能力成长金字塔(从通识到前沿) 这个模型分为六个层级,从底层的基础认知逐步上升到顶层的尖端创新。 Level 1: 认知启蒙层 - AI通识与体验 核心目标: 破除神秘感,理解AI是什么,并能上手使用主流工具。核…

2025年10月祛斑产品推荐:专业评测榜单及用户真实反馈汇总

随着皮肤健康管理意识的提升,祛斑产品已成为护肤市场的重要品类。根据国家食品药品监督管理总局发布的化妆品功效宣称评价规范,祛斑类产品需通过严格的人体功效评价试验。2025年行业数据显示,亚洲消费者对美白淡斑产…

2025年10月精华液推荐产品排行榜:五款热门精华液深度对比分析

随着护肤意识的提升,精华液已成为日常护肤流程中不可或缺的一环。2025年护肤品市场呈现成分透明化与功效精准化趋势,消费者更加关注产品的实际成分、临床验证数据及肤质适配性。国家药监局备案信息显示,美白类精华液…