diff --git a/playSong.py b/playSong.py index 70ce3cb..c2ac5e8 100644 --- a/playSong.py +++ b/playSong.py @@ -1,11 +1,8 @@ -# to build, use "cd (playsong directory)" -# pyinstaller --onefile playSong.py - import pyMIDI import threading import random - from pynput.keyboard import Key, Controller, Listener +import time global isPlaying global infoTuple @@ -15,6 +12,7 @@ global origionalPlaybackSpeed global speedMultiplier global legitModeActive +global heldNotes isPlaying = False legitModeActive = False @@ -23,6 +21,7 @@ elapsedTime = 0 origionalPlaybackSpeed = 1.0 speedMultiplier = 2.0 +heldNotes = {} conversionCases = {'!': '1', '@': '2', '£': '3', '$': '4', '%': '5', '^': '6', '&': '7', '*': '8', '(': '9', ')': '0'} @@ -64,12 +63,12 @@ def onDelPress(): print("Stopping...") def isShifted(charIn): - asciiValue = ord(charIn) - if(asciiValue >= 65 and asciiValue <= 90): - return True - if(charIn in "!@#$%^&*()_+{}|:\"<>?"): - return True - return False + asciiValue = ord(charIn) + if asciiValue >= 65 and asciiValue <= 90: + return True + if charIn in "!@#$%^&*()_+{}|:\"<>?": + return True + return False def speedUp(event): global playback_speed @@ -81,7 +80,6 @@ def slowDown(event): playback_speed /= speedMultiplier print(f"Slowing down: Playback speed is now {playback_speed:.2f}x") - def pressLetter(strLetter): if isShifted(strLetter): if strLetter in conversionCases: @@ -94,16 +92,16 @@ def pressLetter(strLetter): keyboardController.release(strLetter) keyboardController.press(strLetter) return - + def releaseLetter(strLetter): if isShifted(strLetter): if strLetter in conversionCases: - strLetter = conversionCases[strLetter] + strLetter = conversionCases[strLetter] keyboardController.release(strLetter.lower()) else: keyboardController.release(strLetter) return - + def processFile(): global playback_speed with open("song.txt", "r") as macro_file: @@ -151,118 +149,133 @@ def processFile(): print("Error: Tempo not specified") return None - return [tempo, tOffset, processedNotes] + return [tempo, tOffset, processedNotes, []] def floorToZero(i): - if(i > 0): - return i - else: - return 0 + if i > 0: + return i + else: + return 0 # for this method, we instead use delays as l[0] and work using indexes with delays instead of time # we'll use recursion and threading to press keys def parseInfo(): - - tempo = infoTuple[0] - notes = infoTuple[2][1:] - - # parse time between each note - # while loop is required because we are editing the array as we go - i = 0 - while i < len(notes)-1: - note = notes[i] - nextNote = notes[i+1] - if "tempo" in note[1]: - tempo = 60/float(note[1].split("=")[1]) - notes.pop(i) - - note = notes[i] - if i < len(notes)-1: - nextNote = notes[i+1] - else: - note[0] = (nextNote[0] - note[0]) * tempo - i += 1 - - # let's just hold the last note for 1 second because we have no data on it - notes[len(notes)-1][0] = 1.00 - - return notes + tempo = infoTuple[0] + notes = infoTuple[2][1:] + + # parse time between each note + # while loop is required because we are editing the array as we go + i = 0 + while i < len(notes) - 1: + note = notes[i] + nextNote = notes[i + 1] + if "tempo" in note[1]: + tempo = 60 / float(note[1].split("=")[1]) + notes.pop(i) + + note = notes[i] + if i < len(notes) - 1: + nextNote = notes[i + 1] + else: + note[0] = (nextNote[0] - note[0]) * tempo + i += 1 + + # let's just hold the last note for 1 second because we have no data on it + notes[len(notes) - 1][0] = 1.00 + + return notes def adjustTempoForCurrentNote(): global isPlaying, storedIndex, playback_speed, elapsedTime, legitModeActive - tempo_changes = infoTuple[3] + if len(infoTuple) > 3: + tempo_changes = infoTuple[3] - for change in tempo_changes: - if change[0] == storedIndex: - new_tempo = change[1] - playback_speed = new_tempo / origionalPlaybackSpeed - print(f"Tempo changed: New playback speed is {playback_speed:.2f}x") + for change in tempo_changes: + if change[0] == storedIndex: + new_tempo = change[1] + playback_speed = new_tempo / origionalPlaybackSpeed + print(f"Tempo changed: New playback speed is {playback_speed:.2f}x") def playNextNote(): - global isPlaying, storedIndex, playback_speed, elapsedTime, legitModeActive + global isPlaying, storedIndex, playback_speed, elapsedTime, legitModeActive, heldNotes adjustTempoForCurrentNote() + notes = infoTuple[2] total_duration = calculateTotalDuration(notes) - notes = infoTuple[2] if isPlaying and storedIndex < len(notes): noteInfo = notes[storedIndex] delay = floorToZero(noteInfo[0]) + note_keys = noteInfo[1] + # Legit Mode if legitModeActive: delay_variation = random.uniform(0.90, 1.10) delay *= delay_variation if random.random() < 0.05: - if random.random() < 0.5 and len(noteInfo[1]) > 1: - noteInfo[1] = noteInfo[1][1:] + if random.random() < 0.5 and len(note_keys) > 1: + note_keys = note_keys[1:] else: - if storedIndex == 0 or notes[storedIndex-1][0] > 0.3: + if storedIndex == 0 or notes[storedIndex - 1][0] > 0.3: delay += random.uniform(0.1, 0.5) elapsedTime += delay - # Regular Mode - if noteInfo[1][0] == "~": - for n in noteInfo[1][1:]: + # Press or release keys based on the presence of "~" + if "~" in note_keys: + for n in note_keys.replace("~", ""): releaseLetter(n) + if n in heldNotes: + del heldNotes[n] else: - for n in noteInfo[1]: + for n in note_keys: pressLetter(n) + heldNotes[n] = noteInfo[0] + + # Schedule release of held notes + threading.Timer(noteInfo[0] / playback_speed, releaseHeldNotes, [note_keys]).start() - if "~" not in noteInfo[1]: + if "~" not in note_keys: elapsed_mins, elapsed_secs = divmod(elapsedTime, 60) total_mins, total_secs = divmod(total_duration, 60) - print(f"[{int(elapsed_mins)}m {int(elapsed_secs)}s/{int(total_mins)}m {int(total_secs)}s] {noteInfo[1]}") + print(f"[{int(elapsed_mins)}m {int(elapsed_secs)}s/{int(total_mins)}m {int(total_secs)}s] {note_keys}") storedIndex += 1 if delay == 0: playNextNote() else: - threading.Timer(delay/playback_speed, playNextNote).start() + threading.Timer(delay / playback_speed, playNextNote).start() elif storedIndex >= len(notes): isPlaying = False storedIndex = 0 elapsedTime = 0 +def releaseHeldNotes(note_keys): + global heldNotes + for n in note_keys: + if n in heldNotes: + releaseLetter(n) + if n in heldNotes: + del heldNotes[n] + def rewind(KeyboardEvent): - global storedIndex - if storedIndex - 10 < 0: - storedIndex = 0 - - else: - storedIndex -= 10 - print("Rewound to %.2f" % storedIndex) + global storedIndex + if storedIndex - 10 < 0: + storedIndex = 0 + else: + storedIndex -= 10 + print("Rewound to %.2f" % storedIndex) def skip(KeyboardEvent): - global storedIndex - if storedIndex + 10 > len(infoTuple[2]): - isPlaying = False - storedIndex = 0 - else: - storedIndex += 10 - print("Skipped to %.2f" % storedIndex) + global storedIndex + if storedIndex + 10 > len(infoTuple[2]): + isPlaying = False + storedIndex = 0 + else: + storedIndex += 10 + print("Skipped to %.2f" % storedIndex) def onKeyPress(key): global isPlaying, storedIndex, playback_speed, legitModeActive @@ -320,6 +333,6 @@ def main(): with Listener(on_press=onKeyPress) as listener: listener.join() - + if __name__ == "__main__": - main() + main() diff --git a/pyMIDI.py b/pyMIDI.py index d0ef657..909d8c9 100644 --- a/pyMIDI.py +++ b/pyMIDI.py @@ -1,361 +1,362 @@ import os import playSong import sys +import codecs class MidiFile: - startSequence = [ [0x4D,0x54,0x68,0x64], #MThd - [0x4D,0x54,0x72,0x6B], #MTrk - [0xFF] #FF - ] - - typeDict = {0x00 : "Sequence Number", - 0x01 : "Text Event", - 0x02 : "Copyright Notice", - 0x03 : "Sequence/Track Name", - 0x04 : "Instrument Name", - 0x05 : "Lyric", - 0x06 : "Marker", - 0x07 : "Cue Point", - 0x20 : "MIDI Channel Prefix", - 0x2F : "End of Track", - 0x51 : "Set Tempo", - 0x54 : "SMTPE Offset", - 0x58 : "Time Signature", - 0x59 : "Key Signature", - 0x7F : "Sequencer-Specific Meta-event", - 0x21 : "Prefix Port", - 0x20 : "Prefix Channel", - 0x09 : "Other text format [0x09]", - 0x08 : "Other text format [0x08]", - 0x0A : "Other text format [0x0A]", - 0x0C : "Other text format [0x0C]" - } - - - def __init__(self,midi_file,verbose=False,debug=False): - self.verbose = verbose - self.debug = debug - - self.bytes = -1 - self.headerLength = -1 - self.headerOffset = 23 - self.format = -1 - self.tracks = -1 - self.division = -1 - self.divisionType = -1 - self.itr = 0 - self.runningStatus = -1 - self.tempo = 0 - - self.midiRecord_list = [] - self.record_file = "midiRecord.txt" - self.midi_file = midi_file - - self.deltaTimeStarted = False - self.deltaTime = 0 - - self.key_press_count = 0 - - self.virtualPianoScale = list("1!2@34$5%6^78*9(0qQwWeErtTyYuiIoOpPasSdDfgGhHjJklLzZxcCvVbBnm") - - self.startCounter = [0] * len(MidiFile.startSequence) - - self.runningStatusSet = False - - self.events = [] - self.notes = [] - self.success = False - - print("Processing",midi_file) - try: - with open(self.midi_file,"rb") as f: - self.bytes = bytearray(f.read()) - self.readEvents() - print(self.key_press_count,"notes processed") - self.clean_notes() - self.success = True - finally: - self.save_record(self.record_file) - - def checkStartSequence(self): - for i in range(len(self.startSequence)): - if(len(self.startSequence[i]) == self.startCounter[i]): - return True - return False - - def skip(self,i): - self.itr += i - - def readLength(self): - contFlag = True - length = 0 - while(contFlag): - if((self.bytes[self.itr] & 0x80) >> 7 == 0x1): - length = (length << 7) + (self.bytes[self.itr] & 0x7F) - else: - contFlag = False - length = (length << 7) + (self.bytes[self.itr] & 0x7F) - self.itr += 1 - return length - - def readMTrk(self): - length = self.getInt(4) - self.log("MTrk len",length) - self.readMidiTrackEvent(length) - - def readMThd(self): - self.headerLength = self.getInt(4) - self.log("HeaderLength",self.headerLength) - self.format = self.getInt(2) - self.tracks = self.getInt(2) - div = self.getInt(2) - self.divisionType = (div & 0x8000) >> 16 - self.division = div & 0x7FFF - self.log("Format %d\nTracks %d\nDivisionType %d\nDivision %d" % (self.format,self.tracks,self.divisionType,self.division)) - - def readText(self,length): - s = "" - start = self.itr - while(self.itr < length+start): - s += chr(self.bytes[self.itr]) - self.itr+=1 - return s - - def readMidiMetaEvent(self,deltaT): - type = self.bytes[self.itr] - self.itr+=1 - length = self.readLength() - - try: - eventName = self.typeDict[type] - except: - eventName = "Unknown Event " + str(type) - - self.log("MIDIMETAEVENT",eventName,"LENGTH",length,"DT",deltaT) - if(type == 0x2F): - self.log("END TRACK") - self.itr += 2 - return False - elif(type in [0x01,0x02,0x03,0x04,0x05,0x06,0x07,0x08,0x09,0x0A,0x0C]): - self.log("\t",self.readText(length)) - elif(type == 0x51): - tempo = round(60000000/self.getInt(3)) - self.tempo = tempo - - self.notes.append([(self.deltaTime/self.division),"tempo=" + str(tempo)]) - self.log("\tNew tempo is", str(tempo)) - else: - self.itr+= length - return True - - def readMidiTrackEvent(self,length): - self.log("TRACKEVENT") - self.deltaTime = 0 - start = self.itr - continueFlag = True - while(length > self.itr - start and continueFlag): - deltaT= self.readLength() - self.deltaTime += deltaT - - if(self.bytes[self.itr] == 0xFF): - self.itr+= 1 - continueFlag = self.readMidiMetaEvent(deltaT) - elif(self.bytes[self.itr] >= 0xF0 and self.bytes[self.itr] <= 0xF7): - self.runningStatusSet = False - self.runningStatus = -1 - self.log("RUNNING STATUS SET:","CLEARED") - else: - self.readVoiceEvent(deltaT) - self.log("End of MTrk event, jumping from",self.itr,"to",start+length) - self.itr = start+length - - def readVoiceEvent(self,deltaT): - if(self.bytes[self.itr] < 0x80 and self.runningStatusSet): - type = self.runningStatus - channel = type & 0x0F - else: - type = self.bytes[self.itr] - channel = self.bytes[self.itr] & 0x0F - if(type >= 0x80 and type <= 0xF7): - self.log("RUNNING STATUS SET:",hex(type)) - self.runningStatus = type - self.runningStatusSet = True - self.itr += 1 - - if(type >> 4 == 0x9): - #Key press - key = self.bytes[self.itr] - self.itr += 1 - velocity = self.bytes[self.itr] - self.itr += 1 - - map = key - 23 - 12 - 1 - while(map >= len(self.virtualPianoScale)): - map -= 12 - while(map < 0): - map += 12 - - - if(velocity == 0): - #Spec defines velocity == 0 as an alternate notation for key release - self.log(self.deltaTime/self.division,"~"+self.virtualPianoScale[map]) - self.notes.append([(self.deltaTime/self.division),"~"+self.virtualPianoScale[map]]) - else: - #Real keypress - self.log(self.deltaTime/self.division,self.virtualPianoScale[map]) - self.notes.append([(self.deltaTime/self.division),self.virtualPianoScale[map]]) - self.key_press_count += 1 - - elif(type >> 4 == 0x8): - #Key release - key = self.bytes[self.itr] - self.itr += 1 - velocity = self.bytes[self.itr] - self.itr += 1 - - map = key - 23 - 12 - 1 - while(map >= len(self.virtualPianoScale)): - map -= 12 - while(map < 0): - map += 12 - - self.log(self.deltaTime/self.division,"~"+self.virtualPianoScale[map]) - self.notes.append([(self.deltaTime/self.division),"~"+self.virtualPianoScale[map]]) - - elif(not type >> 4 in [0x8,0x9,0xA,0xB,0xD,0xE]): - self.log("VoiceEvent",hex(type),hex(self.bytes[self.itr]),"DT",deltaT) - self.itr +=1 - else: - self.log("VoiceEvent",hex(type),hex(self.bytes[self.itr]),hex(self.bytes[self.itr+1]),"DT",deltaT) - self.itr+=2 - - def readEvents(self): - while(self.itr+1 < len(self.bytes)): - #Reset counters to 0 - for i in range(len(self.startCounter)): - self.startCounter[i] = 0 - - #Get to next event / MThd / MTrk - while(self.itr+1 < len(self.bytes) and not self.checkStartSequence()): - for i in range(len(self.startSequence)): - if(self.bytes[self.itr] == self.startSequence[i][self.startCounter[i]]): - self.startCounter[i] += 1 - else: - self.startCounter[i] = 0 - - if(self.itr+1 < len(self.bytes)): - self.itr += 1 - - if(self.startCounter[0] == 4): - self.readMThd() - elif(self.startCounter[1] == 4): - self.readMTrk() - - def log(self,*arg): - if self.verbose or self.debug: - for s in range(len(arg)): - try: - print(str(arg[s]),end=" ") - self.midiRecord_list.append(str(arg[s]) + " ") - except: - print("[?]",end=" ") - self.midiRecord_list.append("[?] ") - print() - if self.debug: input() - self.midiRecord_list.append("\n") - else: - for s in range(len(arg)): - try: - self.midiRecord_list.append(str(arg[s]) + " ") - except: - self.midiRecord_list.append("[?] ") - self.midiRecord_list.append("\n") - - def getInt(self,i): - k = 0 - for n in self.bytes[self.itr:self.itr+i]: - k = (k << 8) + n - self.itr += i - return k - - def round(i): - up = int(i+1) - down = int(i-1) - if(up - i < i - down): - return up - else: - return down - - def clean_notes(self): - self.notes = sorted(self.notes, key=lambda x: float(x[0])) - - if(self.verbose): - for x in self.notes: - print(x) - - #Combine seperate lines with equal timings - i = 0 - while(i < len(self.notes)-1): - a_time,b_time = self.notes[i][0],self.notes[i+1][0] - if (a_time == b_time): - a_notes,b_notes = self.notes[i][1],self.notes[i+1][1] - if "tempo" not in a_notes and "tempo" not in b_notes and "~" not in a_notes and "~" not in b_notes: - self.notes[i][1] += self.notes[i+1][1] - self.notes.pop(i+1) - else: - i += 1 - else: - i += 1 - - #Remove duplicate notes on same line - for q in range(len(self.notes)): - letterDict = {} - newline = [] - if not "tempo" in self.notes[q][1] and "~" not in self.notes[q][1]: - for i in range(len(self.notes[q][1])): - if(not(self.notes[q][1][i] in letterDict)): - newline.append(self.notes[q][1][i]) - letterDict[self.notes[q][1][i]] = True - self.notes[q][1] = "".join(newline) - return - - def save_song(self,song_file): - print("Saving notes to",song_file) - with open(song_file,"w") as f: - f.write("playback_speed=1.1\n") - for l in self.notes: - f.write(str(l[0]) + " " + str(l[1]) + "\n") - return - - def save_sheet(self,sheet_file): - print("Saving sheets to",sheet_file) - offset = self.notes[0][0] - noteCount = 0 - with open(sheet_file,"w") as f: - for timing,notes in self.notes: - if not "tempo" in notes and "~" not in notes: - if(len(notes) > 1): - note = "["+notes+"]" - else: - note = notes - noteCount += 1 - f.write("%7s " % note) - if(noteCount % 8 == 0): - f.write("\n") - return - - def save_record(self,record_file): - print("Saving processing log to",record_file) - with open(record_file,"w") as f: - for s in self.midiRecord_list: - f.write(s) - return - + startSequence = [ + [0x4D, 0x54, 0x68, 0x64], # MThd + [0x4D, 0x54, 0x72, 0x6B], # MTrk + [0xFF] # FF + ] + + typeDict = { + 0x00: "Sequence Number", + 0x01: "Text Event", + 0x02: "Copyright Notice", + 0x03: "Sequence/Track Name", + 0x04: "Instrument Name", + 0x05: "Lyric", + 0x06: "Marker", + 0x07: "Cue Point", + 0x20: "MIDI Channel Prefix", + 0x2F: "End of Track", + 0x51: "Set Tempo", + 0x54: "SMTPE Offset", + 0x58: "Time Signature", + 0x59: "Key Signature", + 0x7F: "Sequencer-Specific Meta-event", + 0x21: "Prefix Port", + 0x20: "Prefix Channel", + 0x09: "Other text format [0x09]", + 0x08: "Other text format [0x08]", + 0x0A: "Other text format [0x0A]", + 0x0C: "Other text format [0x0C]" + } + + def __init__(self, midi_file, verbose=False, debug=False): + self.verbose = verbose + self.debug = debug + + self.bytes = -1 + self.headerLength = -1 + self.headerOffset = 23 + self.format = -1 + self.tracks = -1 + self.division = -1 + self.divisionType = -1 + self.itr = 0 + self.runningStatus = -1 + self.tempo = 0 + + self.midiRecord_list = [] + self.record_file = "midiRecord.txt" + self.midi_file = midi_file + + self.deltaTimeStarted = False + self.deltaTime = 0 + + self.key_press_count = 0 + + self.virtualPianoScale = list("1!2@34$5%6^78*9(0qQwWeErtTyYuiIoOpPasSdDfgGhHjJklLzZxcCvVbBnm") + + self.startCounter = [0] * len(MidiFile.startSequence) + + self.runningStatusSet = False + + self.events = [] + self.notes = [] + self.success = False + + print("Processing", midi_file) + try: + with open(self.midi_file, "rb") as f: + self.bytes = bytearray(f.read()) + self.readEvents() + print(self.key_press_count, "notes processed") + self.clean_notes() + self.success = True + finally: + self.save_record(self.record_file) + + def checkStartSequence(self): + for i in range(len(self.startSequence)): + if len(self.startSequence[i]) == self.startCounter[i]: + return True + return False + + def skip(self, i): + self.itr += i + + def readLength(self): + contFlag = True + length = 0 + while contFlag: + if (self.bytes[self.itr] & 0x80) >> 7 == 0x1: + length = (length << 7) + (self.bytes[self.itr] & 0x7F) + else: + contFlag = False + length = (length << 7) + (self.bytes[self.itr] & 0x7F) + self.itr += 1 + return length + + def readMTrk(self): + length = self.getInt(4) + self.log("MTrk len", length) + self.readMidiTrackEvent(length) + + def readMThd(self): + self.headerLength = self.getInt(4) + self.log("HeaderLength", self.headerLength) + self.format = self.getInt(2) + self.tracks = self.getInt(2) + div = self.getInt(2) + self.divisionType = (div & 0x8000) >> 16 + self.division = div & 0x7FFF + self.log("Format %d\nTracks %d\nDivisionType %d\nDivision %d" % (self.format, self.tracks, self.divisionType, self.division)) + + def readText(self, length): + s = "" + start = self.itr + while self.itr < length + start: + s += chr(self.bytes[self.itr]) + self.itr += 1 + return s + + def readMidiMetaEvent(self, deltaT): + type = self.bytes[self.itr] + self.itr += 1 + length = self.readLength() + + try: + eventName = self.typeDict[type] + except: + eventName = "Unknown Event " + str(type) + + self.log("MIDIMETAEVENT", eventName, "LENGTH", length, "DT", deltaT) + if type == 0x2F: + self.log("END TRACK") + self.itr += 2 + return False + elif type in [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0C]: + self.log("\t", self.readText(length)) + elif type == 0x51: + tempo = round(60000000 / self.getInt(3)) + self.tempo = tempo + + self.notes.append([(self.deltaTime / self.division), "tempo=" + str(tempo)]) + self.log("\tNew tempo is", str(tempo)) + else: + self.itr += length + return True + + def readMidiTrackEvent(self, length): + self.log("TRACKEVENT") + self.deltaTime = 0 + start = self.itr + continueFlag = True + while length > self.itr - start and continueFlag: + deltaT = self.readLength() + self.deltaTime += deltaT + + if self.bytes[self.itr] == 0xFF: + self.itr += 1 + continueFlag = self.readMidiMetaEvent(deltaT) + elif self.bytes[self.itr] >= 0xF0 and self.bytes[self.itr] <= 0xF7: + self.runningStatusSet = False + self.runningStatus = -1 + self.log("RUNNING STATUS SET:", "CLEARED") + else: + self.readVoiceEvent(deltaT) + self.log("End of MTrk event, jumping from", self.itr, "to", start + length) + self.itr = start + length + + def readVoiceEvent(self, deltaT): + if self.bytes[self.itr] < 0x80 and self.runningStatusSet: + type = self.runningStatus + channel = type & 0x0F + else: + type = self.bytes[self.itr] + channel = self.bytes[self.itr] & 0x0F + if 0x80 <= type <= 0xF7: + self.log("RUNNING STATUS SET:", hex(type)) + self.runningStatus = type + self.runningStatusSet = True + self.itr += 1 + + if type >> 4 == 0x9: + # Key press + key = self.bytes[self.itr] + self.itr += 1 + velocity = self.bytes[self.itr] + self.itr += 1 + + map = key - 23 - 12 - 1 + while map >= len(self.virtualPianoScale): + map -= 12 + while map < 0: + map += 12 + + if velocity == 0: + # Spec defines velocity == 0 as an alternate notation for key release + self.log(self.deltaTime / self.division, "~" + self.virtualPianoScale[map]) + self.notes.append([(self.deltaTime / self.division), "~" + self.virtualPianoScale[map]]) + else: + # Real key press + self.log(self.deltaTime / self.division, self.virtualPianoScale[map]) + self.notes.append([(self.deltaTime / self.division), self.virtualPianoScale[map]]) + self.key_press_count += 1 + + elif type >> 4 == 0x8: + # Key release + key = self.bytes[self.itr] + self.itr += 1 + velocity = self.bytes[self.itr] + self.itr += 1 + + map = key - 23 - 12 - 1 + while map >= len(self.virtualPianoScale): + map -= 12 + while map < 0: + map += 12 + + self.log(self.deltaTime / self.division, "~" + self.virtualPianoScale[map]) + self.notes.append([(self.deltaTime / self.division), "~" + self.virtualPianoScale[map]]) + + elif not type >> 4 in [0x8, 0x9, 0xA, 0xB, 0xD, 0xE]: + self.log("VoiceEvent", hex(type), hex(self.bytes[self.itr]), "DT", deltaT) + self.itr += 1 + else: + self.log("VoiceEvent", hex(type), hex(self.bytes[self.itr]), hex(self.bytes[self.itr + 1]), "DT", deltaT) + self.itr += 2 + + def readEvents(self): + while self.itr + 1 < len(self.bytes): + # Reset counters to 0 + for i in range(len(self.startCounter)): + self.startCounter[i] = 0 + + # Get to next event / MThd / MTrk + while self.itr + 1 < len(self.bytes) and not self.checkStartSequence(): + for i in range(len(self.startSequence)): + if self.bytes[self.itr] == self.startSequence[i][self.startCounter[i]]: + self.startCounter[i] += 1 + else: + self.startCounter[i] = 0 + + if self.itr + 1 < len(self.bytes): + self.itr += 1 + + if self.startCounter[0] == 4: + self.readMThd() + elif self.startCounter[1] == 4: + self.readMTrk() + + def log(self, *arg): + if self.verbose or self.debug: + for s in range(len(arg)): + try: + print(str(arg[s]), end=" ") + self.midiRecord_list.append(str(arg[s]) + " ") + except: + print("[?]", end=" ") + self.midiRecord_list.append("[?] ") + print() + if self.debug: input() + self.midiRecord_list.append("\n") + else: + for s in range(len(arg)): + try: + self.midiRecord_list.append(str(arg[s]) + " ") + except: + self.midiRecord_list.append("[?] ") + self.midiRecord_list.append("\n") + + def getInt(self, i): + k = 0 + for n in self.bytes[self.itr:self.itr + i]: + k = (k << 8) + n + self.itr += i + return k + + def round(i): + up = int(i + 1) + down = int(i - 1) + if up - i < i - down: + return up + else: + return down + + def clean_notes(self): + self.notes = sorted(self.notes, key=lambda x: float(x[0])) + + if self.verbose: + for x in self.notes: + print(x) + + # Combine separate lines with equal timings + i = 0 + while i < len(self.notes) - 1: + a_time, b_time = self.notes[i][0], self.notes[i + 1][0] + if a_time == b_time: + a_notes, b_notes = self.notes[i][1], self.notes[i + 1][1] + if "tempo" not in a_notes and "tempo" not in b_notes and "~" not in a_notes and "~" not in b_notes: + self.notes[i][1] += self.notes[i + 1][1] + self.notes.pop(i + 1) + else: + i += 1 + else: + i += 1 + + for q in range(len(self.notes)): + letterDict = {} + newline = [] + if not "tempo" in self.notes[q][1] and "~" not in self.notes[q][1]: + for i in range(len(self.notes[q][1])): + if not (self.notes[q][1][i] in letterDict): + newline.append(self.notes[q][1][i]) + letterDict[self.notes[q][1][i]] = True + self.notes[q][1] = "".join(newline) + return + + def save_song(self, song_file): + print("Saving notes to", song_file) + with codecs.open(song_file, "w", encoding='utf-8') as f: + f.write("playback_speed=1.1\n") + for l in self.notes: + f.write(str(l[0]) + " " + str(l[1]) + "\n") + return + + def save_sheet(self, sheet_file): + print("Saving sheets to", sheet_file) + note_count = 0 + with codecs.open(sheet_file, "w", encoding='utf-8') as f: + for timing, notes in self.notes: + if not "tempo" in notes and "~" not in notes: + if len(notes) > 1: + note = "[" + notes + "]" + else: + note = notes + note_count += 1 + f.write(f"{note} ") + if note_count % 8 == 0: + f.write("\n") + if note_count % 32 == 0: + f.write("\n\n") + return + + def save_record(self, record_file): + print("Saving processing log to", record_file) + with codecs.open(record_file, "w", encoding='utf-8') as f: + for s in self.midiRecord_list: + f.write(s) + return + def get_file_choice(): midi_folder = 'midi' if not os.path.exists(midi_folder): os.makedirs(midi_folder) - + midList = [f for f in os.listdir(midi_folder) if f.lower().endswith('.mid')] if not midList: print("No MIDI files detected. Please add MIDI files to the 'midi' folder.") @@ -371,7 +372,7 @@ def get_file_choice(): except (IndexError, ValueError): print("Invalid selection. Please try again.") return None - + def runPlaySong(): try: playSong.main() @@ -384,7 +385,7 @@ def main(): if not os.path.exists(midi_file): print(f"Error: file not found '{midi_file}'") return 1 - + if not (".mid" in midi_file or ".mid" in midi_file.lower()): print(f"'{midi_file}' has an incorrect file extension") print("Make sure this file ends in '.mid'") @@ -400,13 +401,13 @@ def main(): print("An error has occurred during processing:\n\n") raise e return 1 - + song_file = "song.txt" sheet_file = "sheetConversion.txt" - + midi.save_song(song_file) midi.save_sheet(sheet_file) runPlaySong() - + if __name__ == "__main__": - main() + main()