2 条评论

  • @ 2026-6-15 17:32:02
    import os
    import tkinter as tk
    from tkinter import ttk, filedialog, messagebox
    import pygame
    from mutagen.mp3 import MP3
    from mutagen.flac import FLAC
    from mutagen.oggvorbis import OggVorbis
    from mutagen.wavpack import WavPack
    import random
    import time
    import re
    
    class MusicPlayer:
        def __init__(self, root):
            self.root = root
            self.root.title("稳定版音乐播放器(支持LRC歌词)")
            self.root.geometry("950x600")  # 加宽窗口以容纳歌词
            self.root.resizable(False, False)
            pygame.mixer.init()
    
            # 音频列表与索引
            self.music_list = []
            self.current_index = -1
            self.play_mode = "order"
            self.is_playing = False
            self.music_length = 0
            self.current_file = ""
    
            # 歌词相关
            self.lyrics = []          # [(时间秒, 文本行), ...]
            self.current_lyric_index = -1
    
            # 进度控制核心变量
            self.dragging = False
            self.drag_pos = 0.0
            self.play_start_time = 0
            self.pause_time = 0
    
            self.suffixs = (".mp3", ".flac", ".wav", ".ogg", ".wv")
            self.create_widgets()
            self.update_progress_loop()
    
        def create_widgets(self):
            # 顶部文件夹选择
            frame_top = ttk.Frame(self.root, padding=10)
            frame_top.pack(fill=tk.X)
            ttk.Button(frame_top, text="选择音乐文件夹", command=self.load_folder).pack(side=tk.LEFT)
            self.folder_var = tk.StringVar(value="未选择文件夹")
            ttk.Label(frame_top, textvariable=self.folder_var).pack(side=tk.LEFT, padx=10)
    
            # 主内容区:左侧歌曲列表,右侧歌词
            main_paned = ttk.PanedWindow(self.root, orient=tk.HORIZONTAL)
            main_paned.pack(fill=tk.BOTH, expand=True, padx=10, pady=5)
    
            # 左侧歌曲列表框架
            left_frame = ttk.Frame(main_paned)
            main_paned.add(left_frame, weight=1)
            ttk.Label(left_frame, text="播放列表", font=("微软雅黑", 10, "bold")).pack(anchor=tk.W, pady=(0,5))
            scroll = ttk.Scrollbar(left_frame)
            scroll.pack(side=tk.RIGHT, fill=tk.Y)
            self.listbox = tk.Listbox(left_frame, yscrollcommand=scroll.set, font=("微软雅黑", 11))
            scroll.config(command=self.listbox.yview)
            self.listbox.pack(fill=tk.BOTH, expand=True)
            self.listbox.bind("<Double-Button-1>", self.double_play)
    
            # 右侧歌词显示区域
            right_frame = ttk.Frame(main_paned)
            main_paned.add(right_frame, weight=1)
            ttk.Label(right_frame, text="LRC 歌词", font=("微软雅黑", 10, "bold")).pack(anchor=tk.W, pady=(0,5))
            # 使用Listbox显示歌词,方便高亮
            lyric_scroll = ttk.Scrollbar(right_frame)
            lyric_scroll.pack(side=tk.RIGHT, fill=tk.Y)
            self.lyric_listbox = tk.Listbox(right_frame, yscrollcommand=lyric_scroll.set,
                                            font=("微软雅黑", 10), selectmode=tk.SINGLE,
                                            bg="#f8f8f8", fg="#333333")
            lyric_scroll.config(command=self.lyric_listbox.yview)
            self.lyric_listbox.pack(fill=tk.BOTH, expand=True)
    
            # 进度条区域
            frame_progress = ttk.Frame(self.root, padding=10)
            frame_progress.pack(fill=tk.X)
            self.time_label = ttk.Label(frame_progress, text="00:00 / 00:00")
            self.time_label.pack(side=tk.RIGHT)
    
            self.progress_bar = ttk.Scale(frame_progress, from_=0, to=0, orient=tk.HORIZONTAL)
            self.progress_bar.pack(fill=tk.X, padx=(0,10))
            # 绑定鼠标事件
            self.progress_bar.bind("<ButtonPress-1>", self.drag_start)
            self.progress_bar.bind("<B1-Motion>", self.drag_moving)
            self.progress_bar.bind("<ButtonRelease-1>", self.drag_end)
    
            # 控制按钮区
            frame_btn = ttk.Frame(self.root, padding=10)
            frame_btn.pack()
            self.btn_prev = ttk.Button(frame_btn, text="上一首", command=self.play_prev)
            self.btn_prev.grid(row=0, column=0, padx=5)
            self.btn_pause = ttk.Button(frame_btn, text="播放", command=self.pause_resume)
            self.btn_pause.grid(row=0, column=1, padx=5)
            self.btn_next = ttk.Button(frame_btn, text="下一首", command=self.play_next)
            self.btn_next.grid(row=0, column=2, padx=5)
    
            self.mode_var = tk.StringVar(value="顺序播放")
            mode_menu = ttk.OptionMenu(frame_btn, self.mode_var, "顺序播放",
                                       "顺序播放", "单曲循环", "随机播放", command=self.change_mode)
            mode_menu.grid(row=0, column=3, padx=15)
    
        def change_mode(self, val):
            mode_map = {"顺序播放": "order", "单曲循环": "loop", "随机播放": "random"}
            self.play_mode = mode_map[val]
    
        def load_folder(self):
            path = filedialog.askdirectory()
            if not path:
                return
            self.folder_var.set(path)
            self.music_list.clear()
            self.listbox.delete(0, tk.END)
            for fname in os.listdir(path):
                full_path = os.path.join(path, fname)
                if full_path.lower().endswith(self.suffixs):
                    self.music_list.append(full_path)
                    self.listbox.insert(tk.END, fname)
            if not self.music_list:
                messagebox.showinfo("提示", "文件夹无支持音频")
            else:
                self.stop_music()
                self.current_index = -1
                self.clear_lyrics()
    
        def clear_lyrics(self):
            """清空歌词显示"""
            self.lyric_listbox.delete(0, tk.END)
            self.lyrics.clear()
            self.current_lyric_index = -1
    
        def load_lyrics(self, music_path):
            """加载与音乐文件同名的.lrc文件"""
            self.clear_lyrics()
            base = os.path.splitext(music_path)[0]
            lrc_path = base + ".lrc"
            if not os.path.exists(lrc_path):
                self.lyric_listbox.insert(tk.END, " (无歌词文件) ")
                return
    
            try:
                with open(lrc_path, "r", encoding="utf-8") as f:
                    lines = f.readlines()
            except UnicodeDecodeError:
                try:
                    with open(lrc_path, "r", encoding="gbk") as f:
                        lines = f.readlines()
                except Exception as e:
                    self.lyric_listbox.insert(tk.END, f"歌词文件读取失败: {e}")
                    return
    
            # 正则匹配时间标签 [mm:ss.xx] 或 [mm:ss]
            time_pattern = re.compile(r'\[(\d{2}):(\d{2})(?:\.(\d{2}))?\]')
            lyric_dict = {}  # 用字典收集同一时间的多行歌词
            for line in lines:
                line = line.strip()
                if not line:
                    continue
                # 查找所有时间标签
                matches = list(time_pattern.finditer(line))
                if not matches:
                    continue
                # 提取歌词文本(去除时间标签后的部分)
                text = line
                for m in matches:
                    text = text.replace(m.group(0), "")
                text = text.strip()
                if not text:
                    continue
                for m in matches:
                    minute = int(m.group(1))
                    sec = int(m.group(2))
                    cent = m.group(3) if m.group(3) else "00"
                    total_sec = minute * 60 + sec + int(cent) / 100
                    # 如果同一时间有多行,合并
                    if total_sec in lyric_dict:
                        lyric_dict[total_sec] += "\n" + text
                    else:
                        lyric_dict[total_sec] = text
    
            # 转换为列表并按时间排序
            self.lyrics = sorted(lyric_dict.items(), key=lambda x: x[0])
            # 填充到Listbox
            for idx, (ts, text) in enumerate(self.lyrics):
                display_line = f"[{self.sec_format(ts)}] {text}"
                self.lyric_listbox.insert(tk.END, display_line)
            if not self.lyrics:
                self.lyric_listbox.insert(tk.END, " (无有效歌词行) ")
    
        def update_lyrics(self, current_time):
            """根据当前播放时间高亮对应歌词行"""
            if not self.lyrics:
                return
    
            # 找到最后一个时间 <= current_time 的歌词行
            idx = -1
            for i, (ts, _) in enumerate(self.lyrics):
                if ts <= current_time:
                    idx = i
                else:
                    break
            if idx == -1:
                return
    
            if idx != self.current_lyric_index:
                # 清除之前的高亮
                if self.current_lyric_index != -1:
                    self.lyric_listbox.itemconfig(self.current_lyric_index, bg="#f8f8f8", fg="#333333")
                self.current_lyric_index = idx
                # 高亮当前行
                self.lyric_listbox.itemconfig(idx, bg="#ffff99", fg="#000000")
                # 滚动到可视区域
                self.lyric_listbox.see(idx)
    
        def get_duration(self, path):
            try:
                if path.endswith(".mp3"):
                    audio = MP3(path)
                elif path.endswith(".flac"):
                    audio = FLAC(path)
                elif path.endswith(".ogg"):
                    audio = OggVorbis(path)
                elif path.endswith(".wv"):
                    audio = WavPack(path)
                else:
                    return 0
                return audio.info.length
            except Exception:
                return 0
    
        def sec_format(self, sec):
            m = int(sec // 60)
            s = int(sec % 60)
            return f"{m:02d}:{s:02d}"
    
        def double_play(self, event):
            sel = self.listbox.curselection()
            if sel:
                self.play_audio(sel[0], start=0)
    
        def play_audio(self, idx, start=0):
            if not self.music_list:
                return
    
            pygame.mixer.music.stop()
            time.sleep(0.05)
    
            self.current_index = idx
            self.current_file = self.music_list[idx]
            pygame.mixer.music.load(self.current_file)
            pygame.mixer.music.play(start=start)
    
            self.is_playing = True
            self.btn_pause.config(text="暂停")
    
            self.music_length = self.get_duration(self.current_file)
            self.progress_bar.config(to=self.music_length)
            self.play_start_time = time.time() - start
    
            self.progress_bar.set(start)
            self.time_label.config(text=f"{self.sec_format(start)} / {self.sec_format(self.music_length)}")
    
            # 高亮歌曲列表
            self.listbox.selection_clear(0, tk.END)
            self.listbox.selection_set(idx)
            self.listbox.see(idx)
    
            # 加载歌词
            self.load_lyrics(self.current_file)
    
        def stop_music(self):
            pygame.mixer.music.stop()
            self.is_playing = False
            self.btn_pause.config(text="播放")
            self.progress_bar.set(0)
            self.time_label.config(text="00:00 / 00:00")
            self.play_start_time = 0
            self.clear_lyrics()
    
        def pause_resume(self):
            if self.current_index == -1:
                if self.music_list:
                    self.play_audio(0)
                return
    
            if self.is_playing:
                self.pause_time = time.time()
                pygame.mixer.music.pause()
                self.is_playing = False
                self.btn_pause.config(text="播放")
            else:
                if self.pause_time > 0:
                    pause_duration = time.time() - self.pause_time
                    self.play_start_time += pause_duration
                    self.pause_time = 0
                pygame.mixer.music.unpause()
                self.is_playing = True
                self.btn_pause.config(text="暂停")
    
        def play_prev(self):
            if not self.music_list:
                return
            new_idx = self.current_index - 1
            if new_idx < 0:
                new_idx = len(self.music_list) - 1
            self.play_audio(new_idx, start=0)
    
        def play_next(self):
            if not self.music_list:
                return
            if self.play_mode == "random":
                new_idx = random.randint(0, len(self.music_list) - 1)
            elif self.play_mode == "loop":
                new_idx = self.current_index
            else:
                new_idx = self.current_index + 1
                if new_idx >= len(self.music_list):
                    new_idx = 0
            self.play_audio(new_idx, start=0)
    
        # 拖拽处理
        def drag_start(self, event):
            self.dragging = True
            self.drag_pos = self.progress_bar.get()
    
        def drag_moving(self, event):
            if self.dragging:
                self.drag_pos = self.progress_bar.get()
                self.time_label.config(text=f"{self.sec_format(self.drag_pos)} / {self.sec_format(self.music_length)}")
    
        def drag_end(self, event):
            if not self.dragging or self.music_length <= 0 or self.current_index == -1:
                self.dragging = False
                return
    
            target_sec = self.progress_bar.get()
            was_playing = self.is_playing
    
            pygame.mixer.music.stop()
            time.sleep(0.05)
            pygame.mixer.music.load(self.current_file)
            pygame.mixer.music.play(start=target_sec)
    
            if was_playing:
                self.is_playing = True
                self.btn_pause.config(text="暂停")
                self.play_start_time = time.time() - target_sec
                self.pause_time = 0
            else:
                pygame.mixer.music.pause()
                self.is_playing = False
                self.btn_pause.config(text="播放")
                self.pause_time = time.time()
    
            self.progress_bar.set(target_sec)
            self.time_label.config(text=f"{self.sec_format(target_sec)} / {self.sec_format(self.music_length)}")
            self.dragging = False
    
            # 拖拽后立即更新歌词显示
            self.update_lyrics(target_sec)
    
        # 定时刷新循环(进度 + 歌词同步)
        def update_progress_loop(self):
            if not self.dragging and self.is_playing and self.music_length > 0 and self.play_start_time > 0:
                current_pos = time.time() - self.play_start_time
                if current_pos >= self.music_length:
                    current_pos = self.music_length
                    self.play_next()
                elif current_pos < 0:
                    current_pos = 0
    
                self.progress_bar.set(current_pos)
                self.time_label.config(text=f"{self.sec_format(current_pos)} / {self.sec_format(self.music_length)}")
                # 同步更新歌词
                self.update_lyrics(current_pos)
    
            self.root.after(100, self.update_progress_loop)
    
    
    if __name__ == "__main__":
        win = tk.Tk()
        app = MusicPlayer(win)
        win.mainloop()
    • @ 2026-5-19 19:31:52

      少羽NB

      • 1