Fbrowser/MidPlay.py

261 lines
9.6 KiB
Python
Executable File

#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_()
"""