Python本地音乐播放器--第三代:带歌词同步功能

发布时间:2026/10/6 18:35:55
Python本地音乐播放器--第三代:带歌词同步功能 先上代码有一点长import tkinter as tk import tkinter.messagebox from tkinter import filedialog import random import pygame import os import re import sys import time import warnings from mutagen.mp3 import MP3 from mutagen.wave import WAVE from mutagen import File from mutagen.flac import FLAC import ctypes import comtypes from comtypes import CLSCTX_ALL from pycaw.pycaw import AudioUtilities, IAudioEndpointVolume # 忽略 pkg_resources 相关的警告 warnings.filterwarnings(ignore, categoryUserWarning, messagepkg_resources is deprecated) def merge_lyrics(lyrics): 合并相同时间戳的原词和翻译词保留原始格式 if not lyrics: return lyrics lines lyrics.split(\n) merged_lines [] i 0 while i len(lines): time_match re.match(r^(\[\d{2}:\d{2}\.\d{2}\])(.*)$, lines[i]) if time_match: current_time time_match.group(1) current_text time_match.group(2).strip() if i1 len(lines): next_time_match re.match(r^(\[\d{2}:\d{2}\.\d{2}\])(.*)$, lines[i1]) if next_time_match and next_time_match.group(1) current_time: next_text next_time_match.group(2).strip() merged_line f{current_time}{current_text} | {next_text} merged_lines.append(merged_line) i 2 continue merged_lines.append(lines[i]) i 1 return \n.join(merged_lines) def extract_lyrics(file_path): 从音频文件中提取歌词并返回合并后的歌词文本 file_path os.path.abspath(file_path) # FLAC文件处理 if file_path.lower().endswith(.flac): try: audio FLAC(file_path) if LYRICS in audio: return merge_lyrics(audio[LYRICS][0]) else: return None except: return None # 其他格式处理 try: audio File(file_path) if not audio: return None # 尝试获取歌词标签 lyrics if hasattr(audio, tags): for tag in audio.tags: if tag.startswith(USLT::) or tag.startswith(TXXX:LYRICS): lyrics str(audio.tags[tag]) break return merge_lyrics(lyrics) if lyrics else None except: return None def clear_line(): 清除当前终端行 sys.stdout.write(\r\033[K) # \r回到行首\033[K清除到行末 sys.stdout.flush() class SystemVolumeController: 系统音量控制器 - Windows 专用版本 def __init__(self): self.volume_interface None self.initialize() def initialize(self): 初始化音量控制接口 try: # 获取系统音频设备 devices AudioUtilities.GetSpeakers() interface devices.Activate( IAudioEndpointVolume._iid_, CLSCTX_ALL, None) self.volume_interface interface.QueryInterface(IAudioEndpointVolume) return True except Exception as e: print(f初始化音量控制失败: {e}) return False def set_volume(self, volume_level): 设置系统音量 if self.volume_interface: try: # 确保音量在0.0到1.0范围内 volume_level max(0.0, min(1.0, volume_level)) # Windows 音量范围是 0.0 到 1.0 self.volume_interface.SetMasterVolumeLevelScalar(volume_level, None) return True except Exception as e: print(f设置系统音量失败: {e}) return False def get_volume(self): 获取当前系统音量 if self.volume_interface: try: return self.volume_interface.GetMasterVolumeLevelScalar() except: return 0.0 return 0.0 class MusicPlayer: def __init__(self, root): self.root root self.root.geometry(420x480) # 增加高度以容纳进度滑块 self.root.title(高级音乐播放器) self.root.resizable(False, False) # 初始化pygame pygame.mixer.init() # 初始化系统音量控制器 self.volume_controller SystemVolumeController() # 初始化播放状态 self.current_index 0 self.song_durations {} self.playlist [] self.path self.is_playing False self.is_paused False self.current_song # 当前播放歌曲 self.lyrics None # 存储歌词内容 self.lyric_entries [] # 存储解析后的歌词条目 self.current_lyric_index 0 # 当前显示的歌词索引 # 时间跟踪变量 self.music_start_time 0 self.last_update_time 0 self.current_position 0 # 当前播放位置秒 self.total_duration 0 # 歌曲总时长 # 创建UI self.create_widgets() # 自动选择目录 self.select_directory() def create_widgets(self): # 顶部控制区 control_frame tk.Frame(self.root) control_frame.pack(pady10, filltk.X, padx10) # 文件路径显示 self.path_label tk.Label(control_frame, text未选择目录, anchortk.W, fggray) self.path_label.pack(filltk.X) # 搜索和播放按钮 search_frame tk.Frame(control_frame) search_frame.pack(filltk.X, pady(5, 0)) self.song_entry tk.Entry(search_frame, width25) self.song_entry.pack(sidetk.LEFT, filltk.X, expandTrue, padx(0, 5)) self.search_btn tk.Button(search_frame, text搜索并准备, commandself.load_selected_song) self.search_btn.pack(sidetk.LEFT) # 播放控制按钮 btn_frame tk.Frame(control_frame) btn_frame.pack(filltk.X, pady(15, 5)) # 整合暂停/恢复按钮为单一按钮 self.play_pause_btn tk.Button( btn_frame, text▶ 播放, width10, height1, commandself.toggle_play_pause ) self.play_pause_btn.pack(sidetk.LEFT, padx2) self.prev_btn tk.Button(btn_frame, text⏮ 上一曲, commandself.play_prev) self.prev_btn.pack(sidetk.LEFT, padx2) self.next_btn tk.Button(btn_frame, text⏭ 下一曲, commandself.play_next) self.next_btn.pack(sidetk.LEFT, padx2) self.random_btn tk.Button(btn_frame, text 随机播放, commandself.play_random) self.random_btn.pack(sidetk.LEFT, padx2) # 状态显示区 status_frame tk.Frame(self.root) status_frame.pack(filltk.X, padx10, pady(10, 0)) self.status_label tk.Label(status_frame, text准备中..., anchortk.W) self.status_label.pack(filltk.X) # 歌曲信息显示 info_frame tk.Frame(self.root) info_frame.pack(filltk.X, padx10, pady5) self.song_name_label tk.Label(info_frame, text当前歌曲: , anchortk.W) self.song_name_label.pack(filltk.X) self.duration_label tk.Label(info_frame, text时长: 00:00, anchortk.W) self.duration_label.pack(filltk.X) # 时间进度显示 time_frame tk.Frame(self.root) time_frame.pack(filltk.X, padx10, pady5) self.time_label tk.Label(time_frame, text00:00 / 00:00, font(Arial, 10, bold)) self.time_label.pack(anchortk.CENTER) # 进度滑块 slider_frame tk.Frame(self.root) slider_frame.pack(filltk.X, padx20, pady5) self.progress_var tk.DoubleVar(value0) self.progress_slider tk.Scale( slider_frame, variableself.progress_var, from_0, to100, orienttk.HORIZONTAL, showvalueFalse, # 隐藏滑块的值显示 length380, commandself.on_slider_changed ) self.progress_slider.bind(ButtonRelease-1, self.on_slider_released) self.progress_slider.pack(filltk.X) # 歌词显示区域 lyrics_frame tk.Frame(self.root) lyrics_frame.pack(filltk.X, padx10, pady(5, 5)) self.lyrics_label tk.Label( lyrics_frame, text歌词显示区域, wraplength380, justifytk.LEFT, anchortk.W, bg#f0f0f0, fg#333, height3, padx5, pady5, relieftk.SUNKEN ) self.lyrics_label.pack(filltk.X) # 音量控制 volume_frame tk.Frame(self.root) volume_frame.pack(filltk.X, padx20, pady(10, 0)) # 获取当前系统音量 current_volume self.volume_controller.get_volume() self.volume_var tk.DoubleVar(valuecurrent_volume) self.volume_scale tk.Scale( volume_frame, variableself.volume_var, from_0.0, to1.0, resolution0.01, orienttk.HORIZONTAL, label音量调节, showvalueFalse, commandself.set_system_volume ) self.volume_scale.pack(filltk.X) self.volume_value tk.Label(volume_frame, textf音量: {int(current_volume * 100)}%) self.volume_value.pack(anchortk.CENTER) # 列表查看按钮 self.view_btn tk.Button(self.root, text 查看播放列表, commandself.view_playlist) self.view_btn.pack(sidetk.BOTTOM, pady10) def set_system_volume(self, volume): 设置系统音量 vol float(volume) if self.volume_controller.set_volume(vol): self.volume_value.config(textf系统音量: {int(vol * 100)}%) else: # 如果设置系统音量失败回退到应用内音量控制 pygame.mixer.music.set_volume(vol) self.volume_value.config(textf应用音量: {int(vol * 100)}%) def on_slider_changed(self, value): 当用户拖动滑块时预览时间但不立即跳转 if not self.is_playing and not self.is_paused: return # 计算预览位置 preview_pos (self.total_duration * float(value)) / 100 # 更新时间预览 current_min, current_sec divmod(int(preview_pos), 60) total_min, total_sec divmod(self.total_duration, 60) time_str f{int(current_min):02d}:{int(current_sec):02d} / {int(total_min):02d}:{int(total_sec):02d} self.time_label.config(texttime_str) # 更新歌词预览 if self.lyric_entries: # 找到预览位置对应的歌词 preview_index 0 for i, (timestamp, _) in enumerate(self.lyric_entries): if preview_pos timestamp: preview_index i else: break # 更新歌词预览 if preview_index len(self.lyric_entries): self.lyrics_label.config(textself.lyric_entries[preview_index][1]) def on_slider_released(self, event): 当用户释放滑块时跳转到指定位置 if not self.is_playing and not self.is_paused: return # 记录当前播放状态 was_playing self.is_playing was_paused self.is_paused # 暂停音乐以便跳转 if was_playing: self.pause_music() # 计算跳转位置百分比 new_pos self.progress_var.get() # 计算实际播放位置秒 new_position (self.total_duration * new_pos) / 100 # 设置新的播放位置 self.current_position new_position self.music_start_time time.time() - new_position # 更新歌词索引 self.current_lyric_index 0 for i, (timestamp, _) in enumerate(self.lyric_entries): if new_position timestamp: self.current_lyric_index i else: break # 更新歌词显示 if self.current_lyric_index len(self.lyric_entries): self.lyrics_label.config(textself.lyric_entries[self.current_lyric_index][1]) # 更新显示的时间 current_min, current_sec divmod(int(self.current_position), 60) total_min, total_sec divmod(self.total_duration, 60) time_str f{int(current_min):02d}:{int(current_sec):02d} / {int(total_min):02d}:{int(total_sec):02d} self.time_label.config(texttime_str) # 恢复播放状态 if was_playing: self.resume_music() # 设置pygame播放位置尝试 if pygame.mixer.music.get_busy(): pygame.mixer.music.set_pos(self.current_position) def select_directory(self): 选择音乐目录 path filedialog.askdirectory(title选择音乐文件夹) if path: self.path path self.path_label.config(textf目录: {os.path.basename(path)}) self.load_playlist() tk.messagebox.showinfo(开始, f已加载目录: {path}) def load_playlist(self): 加载目录中的所有音乐文件 if not self.path: return self.playlist [] for file in os.listdir(self.path): if file.lower().endswith((.mp3, .wav, .flac)): self.playlist.append(file) self.status_label.config(textf找到 {len(self.playlist)} 首歌曲) if self.playlist: self.current_index 0 self.load_song(self.current_index) def load_selected_song(self): 加载选择的歌曲 song_name self.song_entry.get().strip() if not song_name: self.status_label.config(text请输入歌曲名称) return if song_name not in self.playlist: self.status_label.config(textf未找到: {song_name}) return self.current_index self.playlist.index(song_name) self.load_song(self.current_index) self.status_label.config(textf已加载: {song_name}) self.toggle_play_pause() def load_song(self, index): 加载歌曲到播放器并提取歌词 if not self.playlist or index 0 or index len(self.playlist): self.status_label.config(text无效的歌曲索引) return song self.playlist[index] self.current_song song self.song_entry.delete(0, tk.END) self.song_entry.insert(0, song) self.song_name_label.config(textf当前歌曲: {song}) self.progress_var.set(0) # 重置进度滑块 try: file_path os.path.join(self.path, song) # 获取歌曲时长 if song not in self.song_durations: if song.endswith(.mp3): audio MP3(file_path) elif song.endswith(.flac): audio FLAC(file_path) else: # wav audio WAVE(file_path) duration int(audio.info.length) self.song_durations[song] duration duration self.song_durations.get(song, 0) self.total_duration duration mins, secs divmod(duration, 60) self.duration_label.config(textf时长: {int(mins)}:{int(secs):02d}) # 加载音乐 pygame.mixer.music.load(file_path) # 提取歌词 self.lyrics extract_lyrics(file_path) self.parse_lyrics() if self.lyrics and self.lyric_entries: self.lyrics_label.config(text歌词准备完成) else: self.lyrics_label.config(text无歌词内容) self.current_lyric_index 0 except Exception as e: self.status_label.config(textf加载出错: {str(e)}) return def parse_lyrics(self): 解析歌词文本为时间点, 歌词格式 self.lyric_entries [] self.current_lyric_index 0 if not self.lyrics: return # 歌词解析格式 pattern r\[(\d{2}):(\d{2})\.(\d{2})\](.*) for line in self.lyrics.split(\n): match re.match(pattern, line) if match: min, sec, ms, text match.groups() time_sec int(min) * 60 int(sec) int(ms)/100 # 移除歌词文本首尾空格 text text.strip() # 分割原词和翻译词如果存在 parts text.split(|) # 合并到单行保证显示简洁 display_text | .join([part.strip() for part in parts if part.strip()]) self.lyric_entries.append((time_sec, display_text)) # 确保歌词条目按时间顺序排序 self.lyric_entries.sort(keylambda x: x[0]) def update_lyrics_display(self, elapsed): 更新当前显示的歌词 if not self.lyric_entries or not self.is_playing: return # 查找当前应显示的歌词 while self.current_lyric_index len(self.lyric_entries): # 如果当前歌词时间已过尝试显示下一句 if self.current_lyric_index len(self.lyric_entries) - 1: next_time self.lyric_entries[self.current_lyric_index 1][0] if elapsed next_time: self.current_lyric_index 1 else: break else: break # 显示当前歌词 if self.current_lyric_index len(self.lyric_entries): display_text self.lyric_entries[self.current_lyric_index][1] self.lyrics_label.config(textdisplay_text) def toggle_play_pause(self): 切换播放/暂停状态 if not self.playlist: self.status_label.config(text播放列表为空) return if not self.is_playing and not self.is_paused: # 初始状态 self.play_music() elif self.is_playing: # 当前正在播放 self.pause_music() elif self.is_paused: # 当前暂停 self.resume_music() def play_music(self): 开始播放音乐 if pygame.mixer.music.get_busy(): pygame.mixer.music.stop() pygame.mixer.music.play() self.is_playing True self.is_paused False self.status_label.config(text播放中...) self.play_pause_btn.config(text⏸ 暂停) self.start_time_updater() self.current_lyric_index 0 # 重置歌词索引 self.current_position 0 # 重置播放位置 self.music_start_time time.time() # 重置开始时间 def pause_music(self): 暂停播放 if self.is_playing: pygame.mixer.music.pause() self.is_paused True self.is_playing False self.status_label.config(text已暂停) self.play_pause_btn.config(text▶ 继续) def resume_music(self): 恢复播放 if self.is_paused: pygame.mixer.music.unpause() self.is_paused False self.is_playing True self.status_label.config(text播放中...) self.play_pause_btn.config(text⏸ 暂停) # 更新开始时间考虑暂停的时间 self.music_start_time time.time() - self.current_position def play_prev(self): 播放上一首歌曲 if not self.playlist: return self.current_index (self.current_index - 1) % len(self.playlist) self.load_song(self.current_index) self.play_music() def play_next(self): 播放下一首歌曲 if not self.playlist: return self.current_index (self.current_index 1) % len(self.playlist) self.load_song(self.current_index) self.play_music() def play_random(self): 随机播放歌曲 if not self.playlist: return self.current_index random.randint(0, len(self.playlist) - 1) self.load_song(self.current_index) self.play_music() def view_playlist(self): 查看播放列表 if not self.playlist: tk.messagebox.showinfo(播放列表, 播放列表为空) return top tk.Toplevel(self.root) top.title(播放列表) top.geometry(300x400) listbox tk.Listbox(top) listbox.pack(filltk.BOTH, expandTrue, padx10, pady10) scrollbar tk.Scrollbar(listbox) scrollbar.pack(sidetk.RIGHT, filltk.Y) listbox.config(yscrollcommandscrollbar.set) scrollbar.config(commandlistbox.yview) for i, song in enumerate(self.playlist): mark ▶ if song self.current_song else listbox.insert(tk.END, f{i1}. {song}{mark}) # 添加选择按钮 def play_selected(): try: selection listbox.curselection() if selection: self.current_index selection[0] self.load_song(self.current_index) self.play_music() top.destroy() except Exception as e: tk.messagebox.showerror(错误, f播放失败: {e}) select_btn tk.Button(top, text播放选中歌曲, commandplay_selected) select_btn.pack(pady5) def start_time_updater(self): 启动播放进度和歌词更新器 self.music_start_time time.time() self.last_update_time 0 self.current_position 0 def update_time(): try: if self.is_playing and not self.is_paused: # 仅当播放且未暂停时更新 # 计算播放时间 self.current_position time.time() - self.music_start_time elapsed self.current_position song_name self.playlist[self.current_index] total_time self.total_duration # 更新时间显示 if elapsed - self.last_update_time 0.1 or elapsed 0: current_min, current_sec divmod(int(elapsed), 60) total_min, total_sec divmod(total_time, 60) time_str f{int(current_min):02d}:{int(current_sec):02d} / {int(total_min):02d}:{int(total_sec):02d} self.time_label.config(texttime_str) # 更新进度滑块 if total_time 0: progress_percent (elapsed / total_time) * 100 self.progress_var.set(progress_percent) self.last_update_time elapsed # 更新歌词显示 self.update_lyrics_display(elapsed) # 检查歌曲是否结束 if elapsed total_time - 1 and total_time 0: # 1秒容差 self.play_next() else: self.root.after(100, update_time) # 100毫秒后再次更新 elif self.is_paused: # 暂停状态保持定时器 self.root.after(100, update_time) else: # 停止状态清除歌词 self.lyrics_label.config(text播放已停止) except Exception as e: # 捕获异常防止崩溃 print(f更新异常: {e}) self.status_label.config(textf更新错误: {str(e)}) # 启动更新 update_time() if __name__ __main__: root tk.Tk() player MusicPlayer(root) root.mainloop()相较于上一代主要更新了1.界面更整洁2.添加歌词播放效果内嵌3.添加滑块用以调整进度不太稳定但能用以及有一点点长效果如图