import sys import os import pygame import time import re from PyQt6.QtWidgets import ( QApplication, QMainWindow, QWidget, QVBoxLayout, QLabel, QPushButton, QSlider, QHBoxLayout, QFileDialog ) from PyQt6.QtCore import QTimer, Qt, QSize, QPropertyAnimation, QPoint from PyQt6.QtGui import QPixmap, QIcon, QPalette, QColor class MusicPlayer(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle("DreamLife|MusicPlayer") self.setGeometry(300, 200, 400, 500) self.initUI() pygame.mixer.init() self.playlist = [] # 歌曲列表(文件路径) self.current_song_index = -1 # 当前播放歌曲索引 self.lyrics = [] # 解析后的歌词列表 [(时间戳, 歌词)] self.current_lyric_index = 0 # 当前歌词索引 self.is_playing = False # 播放状态 self.music_length = 0 # 音乐总时长(秒) self.start_time = 0 # 播放开始时间(用于计算已播放时间) self.pause_time = 0 # 暂停时记录的播放进度(秒) self.animation = None # 存储滚动动画对象 self.last_lyric = None # 上一次显示的歌词,用于避免重复启动动画 self.music_file = None # 当前音乐文件路径 self.lyrics_file = None # 当前歌词文件路径 self.cover_file = None # 当前封面文件路径 def initUI(self): self.central_widget = QWidget() self.setCentralWidget(self.central_widget) self.layout = QVBoxLayout() self.layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.central_widget.setLayout(self.layout) # 设置背景颜色 self.set_background_color("#2E2E2E") # 封面 self.cover_label = QLabel() self.layout.addWidget(self.cover_label, alignment=Qt.AlignmentFlag.AlignCenter) # 添加一个按钮用于加载外部音乐文件夹 self.load_folder_button = QPushButton("选择音乐文件夹") self.load_folder_button.clicked.connect(self.open_folder_dialog) self.layout.addWidget(self.load_folder_button, alignment=Qt.AlignmentFlag.AlignCenter) # 歌词显示区域(固定大小容器,便于动画处理,不随内容改变大小) self.lyrics_container = QWidget() self.lyrics_container.setFixedSize(300, 50) self.lyrics_container.setStyleSheet("background-color: transparent;") # 歌词标签放置在容器中 self.lyrics_browser = QLabel("", self.lyrics_container) self.lyrics_browser.setAlignment(Qt.AlignmentFlag.AlignCenter) self.lyrics_browser.setStyleSheet("font-size: 24px; color: white; font-weight: bold;") self.lyrics_browser.setFixedHeight(50) self.lyrics_browser.move(0, 0) self.layout.addWidget(self.lyrics_container, alignment=Qt.AlignmentFlag.AlignCenter) # 进度条 self.slider = QSlider(Qt.Orientation.Horizontal) self.slider.setRange(0, 100) self.slider.sliderPressed.connect(self.pause_music) self.slider.sliderReleased.connect(self.seek_music) self.layout.addWidget(self.slider) # 播放控制区域(增加上一首、播放/暂停、下一首) self.control_layout = QHBoxLayout() self.control_layout.setAlignment(Qt.AlignmentFlag.AlignCenter) self.prev_button = QPushButton() self.prev_button.setIcon(QIcon(self.resource_path("file/prev.png"))) self.prev_button.setIconSize(QSize(40, 40)) self.prev_button.setStyleSheet("border: none;") self.prev_button.clicked.connect(self.play_previous_song) self.control_layout.addWidget(self.prev_button) self.play_button = QPushButton() self.play_button.setIcon(QIcon(self.resource_path("file/play.png"))) self.play_button.setIconSize(QSize(52, 52)) self.play_button.setStyleSheet("border: none;") self.play_button.clicked.connect(self.toggle_play_pause) self.control_layout.addWidget(self.play_button) self.next_button = QPushButton() self.next_button.setIcon(QIcon(self.resource_path("file/next.png"))) self.next_button.setIconSize(QSize(40, 40)) self.next_button.setStyleSheet("border: none;") self.next_button.clicked.connect(self.play_next_song) self.control_layout.addWidget(self.next_button) self.layout.addLayout(self.control_layout) # 定时器,每500毫秒更新一次进度和歌词 self.timer = QTimer(self) self.timer.timeout.connect(self.update_lyrics_and_progress) def resource_path(self, relative_path): """ 获取资源文件的路径,适配 PyInstaller 打包模式 """ if getattr(sys, 'frozen', False): base_path = sys._MEIPASS # PyInstaller 运行时解压目录 else: base_path = os.path.dirname(os.path.abspath(__file__)) # 开发模式 return os.path.join(base_path, relative_path) def set_background_color(self, color): palette = self.palette() palette.setColor(QPalette.ColorRole.Window, QColor(color)) self.setPalette(palette) self.central_widget.setStyleSheet(f"background-color: {color};") def load_cover(self): # 如果用户选择了封面文件,优先加载,否则加载默认封面 if self.cover_file and os.path.exists(self.cover_file): pixmap = QPixmap(self.cover_file) else: cover_path = self.resource_path("file/cover.jpg") if os.path.exists(cover_path): pixmap = QPixmap(cover_path) else: self.cover_label.setText("封面未找到") return self.cover_label.setPixmap(pixmap.scaled(256, 256, Qt.AspectRatioMode.KeepAspectRatio)) def load_lyrics(self): # 根据用户选择的歌词文件加载歌词,如果没有选择则使用默认歌词文件 if self.lyrics_file and os.path.exists(self.lyrics_file): path = self.lyrics_file else: # 默认歌词文件(可以根据需要调整) path = self.resource_path("file/默认歌词.lrc") if os.path.exists(path): try: with open(path, 'r', encoding='utf-8') as file: lines = file.readlines() self.lyrics = [] for line in lines: match = re.match(r"\[(\d+):(\d+\.\d+)](.*)", line) if match: minutes = int(match.group(1)) seconds = float(match.group(2)) timestamp = minutes * 60 + seconds lyrics_text = match.group(3).strip() self.lyrics.append((timestamp, lyrics_text)) self.lyrics.sort() except Exception as e: self.lyrics_browser.setText("加载歌词出错: " + str(e)) else: self.lyrics_browser.setText("歌词文件未找到") def open_folder_dialog(self): folder_path = QFileDialog.getExistingDirectory(self, "选择音乐文件夹", "") if folder_path: # 读取该文件夹下所有音乐文件(支持wav和mp3) self.playlist = [] for file in os.listdir(folder_path): if file.lower().endswith(('.wav', '.mp3')): self.playlist.append(os.path.join(folder_path, file)) self.playlist.sort() # 可按文件名排序 if self.playlist: # 将当前索引设置为第一首 self.current_song_index = 0 self.load_current_song_resources() self.play_music() else: self.lyrics_browser.setText("文件夹中没有找到音乐文件") def load_current_song_resources(self): # 设置当前音乐文件路径及相关资源(歌词和封面) self.music_file = self.playlist[self.current_song_index] base, _ = os.path.splitext(self.music_file) lyrics_candidate = base + ".lrc" cover_candidate = base + ".jpg" self.lyrics_file = lyrics_candidate if os.path.exists(lyrics_candidate) else None self.cover_file = cover_candidate if os.path.exists(cover_candidate) else None self.load_cover() self.load_lyrics() def toggle_play_pause(self): if not self.is_playing: if self.pause_time > 0: self.resume_music() else: self.play_music() else: self.pause_music() def play_music(self): if self.music_file and os.path.exists(self.music_file): try: pygame.mixer.music.load(self.music_file) pygame.mixer.music.play(start=0) self.music_length = pygame.mixer.Sound(self.music_file).get_length() self.start_time = time.time() self.pause_time = 0 self.timer.start(500) self.play_button.setIcon(QIcon(self.resource_path("file/pause.png"))) self.is_playing = True except Exception as e: self.lyrics_browser.setText("播放错误: " + str(e)) else: self.lyrics_browser.setText("") def resume_music(self): try: pygame.mixer.music.unpause() self.start_time = time.time() - self.pause_time self.timer.start(500) self.play_button.setIcon(QIcon(self.resource_path("file/pause.png"))) self.is_playing = True except Exception as e: self.lyrics_browser.setText("恢复播放错误: " + str(e)) def pause_music(self): if self.is_playing: self.pause_time = time.time() - self.start_time pygame.mixer.music.pause() self.timer.stop() self.play_button.setIcon(QIcon(self.resource_path("file/play.png"))) self.is_playing = False def seek_music(self): if self.music_length > 0: new_time = self.slider.value() / 100 * self.music_length pygame.mixer.music.play(start=new_time) self.start_time = time.time() - new_time self.pause_time = new_time self.current_lyric_index = self.find_lyric_index(new_time) self.update_lyrics_display() self.timer.start(500) self.play_button.setIcon(QIcon(self.resource_path("file/pause.png"))) self.is_playing = True def play_next_song(self): if self.playlist: self.current_song_index = (self.current_song_index + 1) % len(self.playlist) self.load_current_song_resources() self.play_music() def play_previous_song(self): if self.playlist: self.current_song_index = (self.current_song_index - 1) % len(self.playlist) self.load_current_song_resources() self.play_music() def find_lyric_index(self, current_time): for i, (timestamp, _) in enumerate(self.lyrics): if current_time < timestamp: return max(i - 1, 0) return len(self.lyrics) - 1 def update_lyrics_display(self): if not self.lyrics: self.lyrics_browser.setText("歌词为空") return if self.current_lyric_index < len(self.lyrics): _, lyric = self.lyrics[self.current_lyric_index] if self.last_lyric != lyric: self.last_lyric = lyric if self.lyrics_browser.fontMetrics().horizontalAdvance(lyric) > self.lyrics_container.width(): self.scroll_lyrics(lyric) else: if self.animation is not None: self.animation.stop() self.animation = None self.lyrics_browser.setText(lyric) self.lyrics_browser.adjustSize() self.lyrics_browser.move((self.lyrics_container.width() - self.lyrics_browser.width()) // 2, 0) def scroll_lyrics(self, lyric): self.lyrics_browser.setText(lyric) self.lyrics_browser.adjustSize() label_width = self.lyrics_browser.width() container_width = self.lyrics_container.width() start_pos = QPoint(50, 0) end_pos = QPoint(-label_width, 0) if self.animation is not None: self.animation.stop() self.animation = QPropertyAnimation(self.lyrics_browser, b"pos") duration = 8000 + (label_width - container_width) * 20 self.animation.setDuration(duration) self.animation.setStartValue(start_pos) self.animation.setEndValue(end_pos) self.animation.start() def update_lyrics_and_progress(self): elapsed_time = time.time() - self.start_time self.current_lyric_index = self.find_lyric_index(elapsed_time) self.update_lyrics_display() if self.music_length > 0: progress = int((elapsed_time / self.music_length) * 100) self.slider.setValue(progress) if __name__ == '__main__': app = QApplication(sys.argv) player = MusicPlayer() player.show() sys.exit(app.exec())