#Path: MidPlay.py # Description: A class to play MIDI files and a class to view MIDI files import pygame # Imports import mido import fluidsynth 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 #profiler remove for production def start_profiling(): global pr pr = cProfile.Profile() pr.enable() def stop_profiling(): pr.disable() pr.dump_stats('midi_profile.out') # The pygame.mixer.init() call is necessary to initialize the mixer module # before any sound can be played. The pygame.init() call is necessary maybe. 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.calculate_midi_duration(self.player.current_midi) progress = current_time / total_time * 100 self.progress_bar.setValue(int(progress)) def calculate_midi_duration(self, midi_file): total_duration = 0 for track in midi_file.tracks: track_duration = max([msg.time for msg in track]) if track else 0 total_duration = max(total_duration, track_duration) return total_duration 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 = mido.MidiFile(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_() """