import subprocess
import os
import sys
import tkinter as tk
from tkinter import messagebox, ttk, filedialog
import threading
import platform
import pygame
class MidiPlayerApp:
def __init__(self, root):
self.root = root
self.root.title("MIDI播放器")
self.root.geometry("600x440")
self.root.resizable(False, False)
self.style = ttk.Style()
self.style.configure("TLabel", font=("SimHei", 10))
self.style.configure("TButton", font=("SimHei", 10))
self.style.configure("TEntry", font=("SimHei", 10))
self.fluid_path = ""
self.soundfont = r"a.sf2"
self.midi_file = "送别1.mid"
self.use_custom_soundfont = tk.BooleanVar(value=False)
self.use_system_player = tk.BooleanVar(value=True)
self.process = None
self.is_playing = False
self.pygame_initialized = False
self.create_widgets()
def create_widgets(self):
main_frame = ttk.Frame(self.root, padding=20)
main_frame.pack(fill=tk.BOTH, expand=True)
ttk.Label(main_frame, text="MIDI播放器", font=("SimHei", 16, "bold")).pack(pady=10)
fluid_frame = ttk.Frame(main_frame)
fluid_frame.pack(fill=tk.X, pady=5)
ttk.Label(fluid_frame, text="FluidSynth路径:").pack(side=tk.LEFT, padx=5)
self.fluid_path_var = tk.StringVar(value=self.fluid_path)
self.fluid_entry = ttk.Entry(fluid_frame, textvariable=self.fluid_path_var, width=40)
self.fluid_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
browse_fluid_btn = ttk.Button(fluid_frame, text="浏览...", command=self.browse_fluid_path)
browse_fluid_btn.pack(side=tk.LEFT, padx=5)
use_sys_frame = ttk.Frame(main_frame)
use_sys_frame.pack(fill=tk.X, pady=5)
use_sys_check = ttk.Checkbutton(
use_sys_frame,
text="使用系统默认MIDI播放 (pygame)",
variable=self.use_system_player,
command=self.toggle_system_player
)
use_sys_check.pack(side=tk.LEFT, padx=5)
self.sys_player_note = ttk.Label(
use_sys_frame,
text="(启用后将使用pygame播放MIDI,依赖系统音频驱动)",
foreground="blue",
font=("SimHei", 9)
)
self.sys_player_note.pack(side=tk.LEFT, padx=5)
self.soundfont_frame = ttk.Frame(main_frame)
self.soundfont_frame.pack(fill=tk.X, pady=5)
ttk.Label(self.soundfont_frame, text="SoundFont文件:").pack(side=tk.LEFT, padx=5)
self.soundfont_var = tk.StringVar(value=self.soundfont)
soundfont_entry = ttk.Entry(self.soundfont_frame, textvariable=self.soundfont_var, width=40)
soundfont_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
browse_sf_btn = ttk.Button(self.soundfont_frame, text="浏览...", command=self.browse_soundfont)
browse_sf_btn.pack(side=tk.LEFT, padx=5)
midi_frame = ttk.Frame(main_frame)
midi_frame.pack(fill=tk.X, pady=5)
ttk.Label(midi_frame, text="MIDI文件:").pack(side=tk.LEFT, padx=5)
self.midi_file_var = tk.StringVar(value=self.midi_file)
midi_entry = ttk.Entry(midi_frame, textvariable=self.midi_file_var, width=40)
midi_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True)
browse_midi_btn = ttk.Button(midi_frame, text="浏览...", command=self.browse_midi_file)
browse_midi_btn.pack(side=tk.LEFT, padx=5)
volume_frame = ttk.Frame(main_frame)
volume_frame.pack(fill=tk.X, pady=10)
ttk.Label(volume_frame, text="音量:").pack(side=tk.LEFT, padx=5)
self.volume_var = tk.DoubleVar(value=0.8)
volume_scale = ttk.Scale(volume_frame, variable=self.volume_var, from_=0.0, to=1.0, orient=tk.HORIZONTAL,
length=200)
volume_scale.pack(side=tk.LEFT, padx=5)
self.volume_label = ttk.Label(volume_frame, text=f"{int(self.volume_var.get() * 100)}%")
self.volume_label.pack(side=tk.LEFT, padx=5)
volume_scale.bind("<Motion>", self.update_volume_label)
self.status_var = tk.StringVar()
self.status_var.set("就绪")
status_label = ttk.Label(main_frame, textvariable=self.status_var, foreground="red", font=("SimHei", 12))
status_label.pack(pady=10)
btn_frame = ttk.Frame(main_frame)
btn_frame.pack(fill=tk.X, pady=10)
play_btn = ttk.Button(btn_frame, text="播放", width=15, command=self.play_midi)
play_btn.pack(side=tk.LEFT, padx=5)
stop_btn = ttk.Button(btn_frame, text="停止", width=15, command=self.stop_midi)
stop_btn.pack(side=tk.LEFT, padx=5)
exit_btn = ttk.Button(btn_frame, text="退出", width=15, command=self.root.quit)
exit_btn.pack(side=tk.LEFT, padx=5)
ttk.Label(main_frame, text="注意:确保MIDI文件路径正确且系统音量已打开", font=("SimHei", 9)).pack(side=tk.BOTTOM, pady=10)
self.toggle_system_player()
def toggle_system_player(self):
"""根据复选框状态启用或禁用相关控件"""
use_system = self.use_system_player.get()
self.fluid_entry.configure(state="disabled" if use_system else "normal")
for child in self.soundfont_frame.winfo_children():
child.configure(state="disabled" if use_system else "normal")
for child in self.volume_label.master.winfo_children():
if isinstance(child, ttk.Scale):
child.configure(state="normal")
elif isinstance(child, ttk.Label) and child != self.volume_label:
continue
else:
child.configure(state="normal")
def update_volume_label(self, event):
"""更新音量标签显示"""
self.volume_label.config(text=f"{int(self.volume_var.get() * 100)}%")
if self.use_system_player.get() and self.pygame_initialized:
pygame.mixer.music.set_volume(self.volume_var.get())
def browse_fluid_path(self):
"""浏览并选择FluidSynth路径(仅在不使用系统播放时有效)"""
if self.use_system_player.get():
return
filename = filedialog.askopenfilename(
title="选择FluidSynth可执行文件",
filetypes=[("可执行文件", "*.exe")]
)
if filename:
self.fluid_path_var.set(filename)
def browse_soundfont(self):
"""浏览并选择SoundFont文件(仅在使用FluidSynth时有效)"""
if self.use_system_player.get():
return
filename = filedialog.askopenfilename(
title="选择SoundFont文件",
filetypes=[("SoundFont文件", "*.sf2")]
)
if filename:
self.soundfont_var.set(filename)
def browse_midi_file(self):
"""浏览并选择MIDI文件"""
filename = filedialog.askopenfilename(
title="选择MIDI文件",
filetypes=[("MIDI文件", "*.mid;*.midi")]
)
if filename:
self.midi_file_var.set(filename)
def check_files(self):
"""检查必要文件是否存在"""
self.midi_file = self.midi_file_var.get()
if not os.path.exists(self.midi_file):
messagebox.showerror("错误", f"MIDI文件 '{self.midi_file}' 不存在!")
return False
if not self.use_system_player.get():
self.fluid_path = self.fluid_path_var.get()
self.soundfont = self.soundfont_var.get()
if not os.path.exists(self.fluid_path):
messagebox.showerror("错误", f"FluidSynth程序 '{self.fluid_path}' 不存在!")
return False
if not os.path.exists(self.soundfont):
messagebox.showerror("错误", f"SoundFont文件 '{self.soundfont}' 不存在!")
return False
return True
def play_midi(self):
"""播放MIDI文件"""
if self.is_playing:
return
if not self.check_files():
return
self.status_var.set("正在播放...")
self.is_playing = True
threading.Thread(target=self._run_playback, daemon=True).start()
def _run_playback(self):
"""根据选择的播放方式执行播放"""
try:
if self.use_system_player.get():
self._play_with_pygame()
else:
self._play_with_fluidsynth()
except Exception as e:
error = str(e)
def show_exception():
messagebox.showerror("播放错误", error)
self.root.after(0, show_exception)
finally:
self.root.after(0, self._play_finished)
def _play_with_pygame(self):
"""使用pygame播放MIDI(系统默认音色库)"""
try:
if not self.pygame_initialized:
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
self.pygame_initialized = True
pygame.mixer.music.load(self.midi_file)
pygame.mixer.music.set_volume(self.volume_var.get())
pygame.mixer.music.play()
while pygame.mixer.music.get_busy() and self.is_playing:
pygame.time.delay(100)
except pygame.error as e:
raise Exception(f"pygame播放错误: {str(e)}")
except Exception as e:
raise Exception(f"播放失败: {str(e)}")
def _play_with_fluidsynth(self):
"""使用FluidSynth播放(自定义音色库)"""
command = [self.fluid_path, '-a', 'dsound', '-g', str(self.volume_var.get())]
command.extend([self.soundfont, self.midi_file])
self.process = subprocess.Popen(
command,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE
)
stdout, stderr = self.process.communicate()
if self.process.returncode != 0:
error_msg = stderr.decode('utf-8') if stderr else "FluidSynth执行错误"
raise Exception(error_msg)
def _play_finished(self):
"""播放完成或停止后的清理工作"""
self.is_playing = False
self.process = None
self.status_var.set("就绪")
if self.use_system_player.get() and self.pygame_initialized:
pygame.mixer.music.stop()
def stop_midi(self):
"""停止播放"""
if not self.is_playing:
return
self.is_playing = False
if self.use_system_player.get() and self.pygame_initialized:
pygame.mixer.music.stop()
elif self.process:
try:
self.process.terminate()
try:
self.process.wait(timeout=2)
except subprocess.TimeoutExpired:
self.process.kill()
except Exception as e:
messagebox.showerror("错误", f"停止播放时出错: {str(e)}")
self._play_finished()
if __name__ == "__main__":
root = tk.Tk()
app = MidiPlayerApp(root)
root.mainloop()