#Path: MidPlay.py # Description: A class to play MIDI files and a class to view MIDI files # probably switching to a different library for midi handling # pretty_midi is not very good for this purpose or real-time playback of midi files """Pretty Midi module type stubs are included but incomplete. Pretty Midi comes with a statement to cite the following paper when used in a research project: Colin Raffel and Daniel P. W. Ellis. Intuitive Analysis, Creation and Manipulation of MIDI Data with pretty_midi. In Proceedings of the 15th International Conference on Music Information Retrieval Late Breaking and Demo Papers, 2014. colinraffel.com/publications/ismir2014intuitive.pdf """ import pygame # Imports import pretty_midi import fluidsynth import sys import os from PyQt5.QtWidgets import (QApplication, QLabel, QListWidget, QFileDialog, QMessageBox, QWidget, QPushButton, QHBoxLayout, QVBoxLayout, QProgressBar, QSlider) # structured for readability and to avoid long lines and it annoys my friend XD from PyQt5.QtCore import QTimer, Qt import threading import cProfile # profiler remove for production pygame.mixer.init() pygame.init() class MidPlayGUI(QWidget): def __init__(self): super().__init__() self.player = MidPlay() self.current_midi_label = QLabel() self.playlist_widget = QListWidget() self.setWindowTitle("MidPlay - Midi Player") self.init_ui() self.timer = QTimer() self.timer.timeout.connect(self.handle_song_end) self.timer.start(1000) def set_volume(self, value): volume = value / 100 pygame.mixer.music.set_volume(volume) def update_progress(self): if self.player.current_midi: current_time = pygame.mixer.music.get_pos() / 1000 # get_pos returns time in milliseconds NOT SECONDS! total_time = self.player.current_midi.get_end_time() progress = current_time / total_time * 100 self.progress_bar.setValue(int(progress)) def handle_song_end(self): if self.player.playing and not pygame.mixer.music.get_busy(): self.player.next_song() if self.player.playlist: self.player.current_index %= len(self.player.playlist) filepath = self.player.playlist[self.player.current_index] filename = os.path.basename(filepath) self.current_midi_label.setText(f"Current MIDI: {filename}") self.update_progress() def init_ui(self): #label = QLabel("MidPlay - Midi player") #label.setStyleSheet("font-size: 20px; font-weight: bold;") self.progress_bar = QProgressBar() self.volume_slider = QSlider(Qt.Horizontal) self.volume_slider.setMinimum(0) self.volume_slider.setMaximum(100) self.volume_slider.setValue(100) self.volume_slider.valueChanged.connect(self.set_volume) self.current_midi_label.setText("Current MIDI: None") self.playlist_widget.itemDoubleClicked.connect(self.play_selected_song) pygame.mixer.music.set_endevent(pygame.USEREVENT) # Buttons play_button = QPushButton("Play") play_button.clicked.connect(self.player.play_midi) pause_button = QPushButton("Pause") pause_button.clicked.connect(self.player.pause) stop_button = QPushButton("Stop") stop_button.clicked.connect(self.player.stop) next_button = QPushButton("Next") next_button.clicked.connect(self.player.next_song) back_button = QPushButton("Back") back_button.clicked.connect(self.previous_song) add_button = QPushButton("Add to Playlist") add_button.clicked.connect(self.load_midi_file) add_folder_button = QPushButton("Add Folder to Playlist") add_folder_button.clicked.connect(self.load_folder) clear_button = QPushButton("Clear Playlist") clear_button.clicked.connect(self.clear_playlist) # Window layout layout = QVBoxLayout() layout.addWidget(self.current_midi_label) layout.addWidget(self.playlist_widget) layout.addWidget(self.progress_bar) layout.addWidget(self.volume_slider) layout.addWidget(play_button) layout.addWidget(pause_button) layout.addWidget(stop_button) layout.addWidget(next_button) layout.addWidget(back_button) layout.addWidget(add_button) layout.addWidget(add_folder_button) layout.addWidget(clear_button) progress_volume_layout = QHBoxLayout() progress_volume_layout.addWidget(self.progress_bar) progress_volume_layout.addWidget(self.volume_slider) layout.addLayout(progress_volume_layout) self.setLayout(layout) # Event handlers def play_selected_song(self, item): index = self.playlist_widget.row(item) self.current_index = index filepath = self.player.playlist[self.current_index] self.player.load_midi(filepath) self.player.play_midi() pygame.mixer.music.set_endevent(pygame.USEREVENT) filename = os.path.basename(filepath) self.current_midi_label.setText(f"Current MIDI: {filename}") def load_midi_file(self): filepath, _ = QFileDialog.getOpenFileName(self, "Select MIDI File", filter="MIDI files (*.mid *.midi)") if filepath: filename = os.path.basename(filepath) self.player.load_midi(filepath) self.current_midi_label.setText(f"Current MIDI: {filename}") self.player.play_midi() self.playlist_widget.addItem(filename) self.player.add_to_playlist(filepath) def load_folder(self): folder = QFileDialog.getExistingDirectory(self, "Select Folder") if folder: for file in os.listdir(folder): if file.endswith((".midi", ".mid")): filepath = os.path.join(folder, file) self.playlist_widget.addItem(file) self.player.add_to_playlist(filepath) # Only add to playlist, don't load immediately!!!!!!!!!!!!!!! #probably should be in the MidPlay class def previous_song(self): if self.player.playlist: filepath = self.player.playlist[self.current_index] filename = os.path.basename(filepath) self.player.current_index = (self.player.current_index - 1) % len(self.player.playlist) self.current_midi_label.setText(f"Current MIDI: {filename}") self.player.play_midi() def clear_playlist(self): self.player.clear_playlist() self.playlist_widget.clear() def closeEvent(self, event): confirmation = QMessageBox.question(self, "Exit Confirmation", "Are you sure you want to exit?", QMessageBox.Yes | QMessageBox.No) if confirmation == QMessageBox.Yes: pygame.mixer.quit() pygame.quit() event.accept() else: event.ignore() class MidPlay: """The Heart of Midi Playback""" def __init__(self): self.playlist = [] self.current_midi = None self.playing = False self.current_index = 0 def load_midi(self, filepath: str) -> None: def load(): try: self.current_midi = pretty_midi.PrettyMIDI(filepath) pygame.mixer.music.load(filepath) except Exception as e: print(f"Error loading MIDI: {e}") threading.Thread(target=load).start() def add_to_playlist(self, filepath: str) -> None: self.playlist.append(filepath) def clear_playlist(self) -> None: self.playlist = [] def play_midi(self) -> None: def play(): if self.current_midi: self.current_midi.instruments[0].synthesize() pygame.mixer.music.play() self.playing = True pygame.mixer.music.set_endevent(pygame.USEREVENT) else: print("No MIDI file loaded") threading.Thread(target=play).start() def pause(self) -> None: pygame.mixer.music.pause() self.playing = False def stop(self) -> None: pygame.mixer.music.stop() self.playing = False def next_song(self) -> None: #print("Debug: next_song() called", self.playlist) debug line if self.playlist: self.current_index = (self.current_index + 1) % len(self.playlist) filepath = self.playlist[self.current_index] # If a new MIDI was loaded before the last one ended, respect that as the new playlist start if self.current_midi and self.playing: # print("Debug: New MIDI loaded before last one ended") # debug line self.load_midi(filepath) self.play_midi() # print("Debug: Filepath:", filepath) # debug line # print("Debug: Current MIDI:", self.current_midi) # debug line if __name__ == '__main__': app = QApplication([]) player_gui = MidPlayGUI() player_gui.show() running = True while True: for event in pygame.event.get(): if event.type == pygame.USEREVENT: player_gui.player.next_song() if event.type == pygame.QUIT: running = False break app.exec_()