255 lines
9.5 KiB
Python
Executable File
255 lines
9.5 KiB
Python
Executable File
#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_() |