音频转换合并切割工具

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

 

posted @ 2025-10-28 14:10  *感悟人生*  阅读(8)  评论(0)    收藏  举报