Python+Tkinter制作音频格式转换器
我们将使用Python的Tkinter库来构建一个音频格式转换器界面。由于音频转换需要实际的处理,我们将使用`pydub`库(需要安装)来进行音频格式转换。同时,我们会使用`ffmpeg`作为后端,因此请确保系统中已安装ffmpeg并添加到环境变量,或者我们将提供一种方式让用户指定ffmpeg路径。 功能要求:
1. 支持选择多个音频文件(批量转换)或单个文件(单独转换)
2. 支持选择输出格式(如mp3, wav, flac, aac等)
3. 支持选择输出目录
4. 开始转换按钮,显示转换进度(由于批量转换,我们可以用进度条或者日志方式)
5. 转换过程中显示转换状态(成功/失败) 界面设计:
- 文件选择:一个按钮用于添加文件,一个按钮用于添加文件夹(批量添加),一个列表显示已选文件。
- 输出格式选择:下拉菜单(Combobox)列出支持的格式。
- 输出目录选择:一个按钮用于选择目录,一个标签显示当前选择的目录。
- 转换按钮:开始转换。
- 日志区域:用于显示转换进度和结果(可以用Text组件)。
步骤:
1. 导入必要的库:tkinter, ttk, filedialog, messagebox, 以及pydub(需要安装:pip install pydub)。
2. 确保ffmpeg可用。如果不可用,可以提示用户安装或指定路径(我们可以在程序初始化时设置pydub的ffmpeg路径,但为了简便,我们假设已安装)。
3. 创建主窗口,设置标题。
4. 创建并布局各个组件。 注意:由于音频转换可能耗时,我们使用多线程来避免界面卡死。 我们将创建以下类: - AudioConverterApp: 主应用类,包含UI和事件处理。
具体实现: 首先,我们导入必要的库:
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import os
import threading
from pygame import mixer
from pydub import AudioSegment
注意:需要用到ffmpeg工具,请提前下载,代码需要调用该工具进行格式转换(可设置全局环境变量,这样可直接调用,也可放入代码包里面,引用包路径即可)
#局部引入
base_dir = os.path.dirname(os.path.abspath(__file__))
ffmpeg_path = os.path.join(base_dir, "ffmpeg", "bin", "ffmpeg.exe")
AudioSegment.converter = ffmpeg_path
编写主体代码:
class AudioConverterApp:def __init__(self, root):self.root = rootself.root.title("音频格式转换器")self.root.geometry("800x800")self.root.resizable(True, True)# 设置应用图标try:self.root.iconbitmap("audio_icon.ico")except:pass# 创建样式self.style = ttk.Style()self.style.configure('TFrame', background='#f0f0f0')self.style.configure('Header.TLabel', font=('Arial', 16, 'bold'), background='#4a6baf', foreground='white')self.style.configure('TButton', font=('Arial', 10), padding=5)self.style.map('TButton', background=[('active', '#4a6baf')])self.style.configure('Progress.Horizontal.TProgressbar', thickness=20, background='#4a6baf')# 创建主框架self.main_frame = ttk.Frame(root)self.main_frame.pack(fill=tk.BOTH, expand=True, padx=20, pady=20)# 创建标题self.header_frame = ttk.Frame(self.main_frame)self.header_frame.pack(fill=tk.X, pady=(0, 20))self.title_label = ttk.Label(self.header_frame,text="音频格式转换器",style='Header.TLabel',anchor=tk.CENTER)self.title_label.pack(fill=tk.X, ipady=10)# 创建控制面板self.control_frame = ttk.LabelFrame(self.main_frame, text="转换设置")self.control_frame.pack(fill=tk.X, pady=10)# 输入文件选择self.input_frame = ttk.Frame(self.control_frame)self.input_frame.pack(fill=tk.X, padx=10, pady=10)ttk.Label(self.input_frame, text="输入文件:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))self.input_entry = ttk.Entry(self.input_frame, width=50)self.input_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)self.browse_button = ttk.Button(self.input_frame,text="浏览文件...",command=self.browse_files,width=10)self.browse_button.grid(row=0, column=2, padx=(5, 0))# 输出格式选择self.format_frame = ttk.Frame(self.control_frame)self.format_frame.pack(fill=tk.X, padx=10, pady=10)ttk.Label(self.format_frame, text="输出格式:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))self.format_var = tk.StringVar(value="mp3")self.format_combobox = ttk.Combobox(self.format_frame,textvariable=self.format_var,width=8,state="readonly")self.format_combobox['values'] = ('mp3', 'wav', 'ogg', 'flac', 'aac', 'm4a')self.format_combobox.grid(row=0, column=1, sticky=tk.W, padx=5)# 输出目录选择self.output_frame = ttk.Frame(self.control_frame)self.output_frame.pack(fill=tk.X, padx=10, pady=10)ttk.Label(self.output_frame, text="输出目录:").grid(row=0, column=0, sticky=tk.W, padx=(0, 5))self.output_entry = ttk.Entry(self.output_frame, width=50)self.output_entry.grid(row=0, column=1, sticky=tk.EW, padx=5)self.output_button = ttk.Button(self.output_frame,text="浏览目录...",command=self.browse_output,width=10)self.output_button.grid(row=0, column=2, padx=(5, 0))# 添加批量转换区域self.batch_frame = ttk.LabelFrame(self.control_frame, text="批量转换")self.batch_frame.pack(fill=tk.X, padx=10, pady=10)self.batch_button = ttk.Button(self.batch_frame,text="添加文件夹...",command=self.browse_folder,width=15)self.batch_button.pack(side=tk.LEFT, padx=5, pady=5)self.clear_button = ttk.Button(self.batch_frame,text="清空列表",command=self.clear_file_list,width=10)self.clear_button.pack(side=tk.RIGHT, padx=5, pady=5)# 文件列表self.list_frame = ttk.LabelFrame(self.main_frame, text="待转换文件")self.list_frame.pack(fill=tk.BOTH, expand=True, pady=10)# 创建带滚动条的文件列表self.scrollbar = ttk.Scrollbar(self.list_frame)self.scrollbar.pack(side=tk.RIGHT, fill=tk.Y)self.file_list = tk.Listbox(self.list_frame,yscrollcommand=self.scrollbar.set,selectmode=tk.EXTENDED,height=8)self.file_list.pack(fill=tk.BOTH, expand=True, padx=5, pady=5)self.scrollbar.config(command=self.file_list.yview)# 状态和进度条self.status_frame = ttk.Frame(self.main_frame)self.status_frame.pack(fill=tk.X, pady=(10, 0))self.progress_var = tk.DoubleVar()self.progress_bar = ttk.Progressbar(self.status_frame,variable=self.progress_var,style='Progress.Horizontal.TProgressbar',mode='determinate',length=500)self.progress_bar.pack(fill=tk.X, pady=5)self.status_var = tk.StringVar(value="就绪")self.status_label = ttk.Label(self.status_frame,textvariable=self.status_var,anchor=tk.W)self.status_label.pack(fill=tk.X)# 控制按钮self.button_frame = ttk.Frame(self.main_frame)self.button_frame.pack(fill=tk.X, pady=10)self.convert_button = ttk.Button(self.button_frame,text="开始转换",command=self.start_conversion,width=15)self.convert_button.pack(side=tk.LEFT, padx=5)self.play_button = ttk.Button(self.button_frame,text="播放音频",command=self.play_audio,width=15,state=tk.DISABLED)self.play_button.pack(side=tk.LEFT, padx=5)self.cancel_button = ttk.Button(self.button_frame,text="取消",command=self.cancel_conversion,width=15)self.cancel_button.pack(side=tk.RIGHT, padx=5)# 初始化变量self.files_to_convert = []self.converting = Falseself.cancel_requested = False# 初始化音频播放器mixer.init()# 配置网格权重self.input_frame.columnconfigure(1, weight=1)self.output_frame.columnconfigure(1, weight=1)def browse_files(self):files = filedialog.askopenfilenames(title="选择音频文件",filetypes=(("音频文件", "*.mp3 *.wav *.ogg *.flac *.aac *.m4a"),("所有文件", "*.*")))if files:self.files_to_convert.extend(files)self.update_file_list()def browse_folder(self):folder = filedialog.askdirectory(title="选择包含音频文件的文件夹")if folder:# 查找文件夹中的音频文件audio_files = []for root, _, files in os.walk(folder):for file in files:if file.lower().endswith(('.mp3', '.wav', '.ogg', '.flac', '.aac', '.m4a')):audio_files.append(os.path.join(root, file))if audio_files:self.files_to_convert.extend(audio_files)self.update_file_list()else:messagebox.showinfo("无音频文件", "所选文件夹中没有找到支持的音频文件")def browse_output(self):folder = filedialog.askdirectory(title="选择输出文件夹")if folder:self.output_entry.delete(0, tk.END)self.output_entry.insert(0, folder)def update_file_list(self):self.file_list.delete(0, tk.END)for file in self.files_to_convert:self.file_list.insert(tk.END, os.path.basename(file))def clear_file_list(self):self.files_to_convert = []self.file_list.delete(0, tk.END)def start_conversion(self):if not self.files_to_convert:messagebox.showwarning("无文件", "请先添加要转换的音频文件")returnoutput_folder = self.output_entry.get().strip()if not output_folder:messagebox.showwarning("无输出目录", "请选择输出目录")returnif not os.path.exists(output_folder):try:os.makedirs(output_folder)except Exception as e:messagebox.showerror("错误", f"无法创建输出目录: {str(e)}")return# 禁用控制按钮self.convert_button.config(state=tk.DISABLED)self.browse_button.config(state=tk.DISABLED)self.output_button.config(state=tk.DISABLED)self.batch_button.config(state=tk.DISABLED)self.clear_button.config(state=tk.DISABLED)self.play_button.config(state=tk.DISABLED)# 重置进度条self.progress_var.set(0)self.converting = Trueself.cancel_requested = False# 在后台线程中运行转换threading.Thread(target=self.convert_files, args=(output_folder,), daemon=True).start()def convert_files(self, output_folder):total_files = len(self.files_to_convert)success_count = 0failed_files = []for i, file_path in enumerate(self.files_to_convert):if self.cancel_requested:breakself.status_var.set(f"正在转换: {os.path.basename(file_path)} ({i + 1}/{total_files})")self.progress_var.set(i / total_files * 100)try:#引入ffmpeg工具路径,非全局变量,如果为全局变量,则下方三行代码可注掉base_dir = os.path.dirname(os.path.abspath(__file__))ffmpeg_path = os.path.join(base_dir, "ffmpeg", "bin", "ffmpeg.exe") AudioSegment.converter = ffmpeg_pathsound = AudioSegment.from_file(file_path)output_format = self.format_var.get()output_path = os.path.join(output_folder,os.path.splitext(os.path.basename(file_path))[0] + '.' + output_format)sound.export(output_path, format=output_format)success_count += 1except Exception as e:failed_files.append(os.path.basename(file_path))print(f"转换失败: {str(e)}")# 更新进度self.progress_var.set((i + 1) / total_files * 100)# 更新UIself.root.after(0, self.conversion_completed, success_count, total_files, failed_files)def conversion_completed(self, success_count, total_files, failed_files):self.converting = Falseself.progress_var.set(100)# 启用按钮self.convert_button.config(state=tk.NORMAL)self.browse_button.config(state=tk.NORMAL)self.output_button.config(state=tk.NORMAL)self.batch_button.config(state=tk.NORMAL)self.clear_button.config(state=tk.NORMAL)self.play_button.config(state=tk.NORMAL)# 显示结果if success_count == total_files:self.status_var.set(f"转换完成! 成功转换 {success_count} 个文件")messagebox.showinfo("完成", f"成功转换 {success_count} 个文件")else:self.status_var.set(f"转换完成! 成功: {success_count}, 失败: {total_files - success_count}")if failed_files:failed_list = "\n".join(failed_files)messagebox.showwarning("部分完成",f"成功转换 {success_count} 个文件\n失败 {len(failed_files)} 个文件:\n{failed_list}")def cancel_conversion(self):if self.converting:self.cancel_requested = Trueself.status_var.set("正在取消转换...")else:self.root.destroy()def play_audio(self):if not self.files_to_convert:messagebox.showwarning("无文件", "没有可播放的音频文件")return# 获取选中的文件selected_indices = self.file_list.curselection()if not selected_indices:messagebox.showwarning("未选择", "请先选择一个音频文件")returnselected_index = selected_indices[0]if 0 <= selected_index < len(self.files_to_convert):file_path = self.files_to_convert[selected_index]try:# 停止当前播放if mixer.music.get_busy():mixer.music.stop()# 加载并播放音频mixer.music.load(file_path)mixer.music.play()self.status_var.set(f"正在播放: {os.path.basename(file_path)}")except Exception as e:messagebox.showerror("播放错误", f"无法播放音频文件: {str(e)}")if __name__ == "__main__":root = tk.Tk()app = AudioConverterApp(root)root.mainloop()
UI界面效果: