def parse(input_file, output_file, new_velocity, align_margin, collated, normalized_tempo, create_channels, index_patches): # TODO: Extract parts of parse function into other functions # TODO: Options for merging tracks # TODO: Align notes in different tracks # TODO: Use channels to split tracks # ===================== # Initialization # ===================== # Check if the input file wasn't given if (input_file == ""): # If the input file weren't given, return an exception return Exception("Input file not specified") # Check if the input file wasn't given if (output_file == ""): # If the input file weren't given, return an exception return Exception("Output file not specified") # If index_patches is selected but not create_channels if (index_patches and not create_channels): return Exception("Patches cannot be indexed without creating channels") # Create lists for storing notes and meta messages track_notes = [] track_meta = [] # Create a list for storing final tracks output_tracks = [] # Create a list to store which output tracks are only meta meta_track_indices = [] # Create a dictionary to store the patch of each track patch_dictionary = {} # Create a list to store which indices tracks are split into split_indices = [] # Try to load the input file try: # Load the input MIDI file input_song = mido.MidiFile(input_file) except Exception as err: # Print the error traceback.print_tb(err.__traceback__) # If it couldn't be loaded, return an error return Exception( "Input file could not be loaded, try checking the input file path") # Create a new MIDI file for the final song output_song = mido.MidiFile(ticks_per_beat=input_song.ticks_per_beat) # Check if we should override the note velocity try: # See if the user has input an integer new_velocity = int(new_velocity) except: # If the user has not input an integer, ignore it new_velocity = -1 pass # If it's out of range, set it to -1 if (new_velocity < 1 or new_velocity > 127): new_velocity = -1 # Check if we should override the tempo try: # See if the user has input an integer normalized_tempo = int(normalized_tempo) # Convert the tempo from bpm to the correct units normalized_tempo = mido.bpm2tempo(normalized_tempo) except: # If the user has not inputted an integer, ignore it normalized_tempo = -1 pass # If it's out of range, set it to -1 if (normalized_tempo < 1): normalized_tempo = -1 # Cast the parameter to a boolean create_channels = bool(create_channels) # Cast the parameter to a boolean index_patches = bool(index_patches) # Create a dictionary to serve as a look up table for tempo tempo_dict = {} # Create a aligning margin variable alignment_margin = 0 try: # See if the user has input a number alignment_margin = float(align_margin) except: pass # ============================ # Extract Tempo Messages # ============================ # Loop through all tracks for i, track in enumerate(input_song.tracks): # Create a time variable for storing absolute time and set it to 0 tick_time = 0 # Loop through all notes in the track for j, msg in enumerate(track): # If there is a change in time then add that to the absolute time tick_time += msg.time # If we found a set_tempo message if (msg.is_meta and msg.type == "set_tempo"): # Add it to the look up table tempo_dict[tick_time] = msg.tempo # If there are no tempo messages if (len(tempo_dict) == 0): # Set the tempo to the default of 120bpm tempo_dict[0] = mido.bpm2tempo(120) # ========================== # Loop Through Tracks # ========================== for i, track in enumerate(input_song.tracks): # =================================== # Program/Patch Change Messages # =================================== # Create a time variable for storing absolute time and set it to 0 tick_time = 0 # Create a list of channels channel_list = [] # Loop through all messages for msg in track: # If this message is not a meta message if not msg.is_meta: try: # If this message's channel isn't in the no_patches dictionary if not msg.channel in channel_list: # Add it channel_list.append(msg.channel) except Exception: pass # If there is a change in time then add that to the absolute time tick_time += msg.time # If this message is a patch change message if (msg.type == "program_change"): # If this channel doesn't have a dictionary entry if not msg.channel in patch_dictionary: # Make an entry patch_dictionary[msg.channel] = {} # Add a patch entry for the current channel at the current time patch_dictionary[msg.channel][tick_time] = msg.program # Loop through all channels for channel in channel_list: # If this channel doesn't have a dictionary entry if not channel in patch_dictionary: # Make an entry patch_dictionary[channel] = {} # If this channel doesn't have a patch at the beginning if not 0 in patch_dictionary[channel]: # Add the default patch at the beginning patch_dictionary[channel][0] = 0 # ============================== # Raw Message Extraction # ============================== # Store all meta messages in a list meta_messages = [msg for msg in track if msg.is_meta] # Reset tick_time to zero tick_time = 0 # Create a new empty list inside the main list for all tracks to append notes to track_meta.append([]) # Create a new empty list inside the main list for all tracks to append meta messages to track_notes.append([]) # Loop through all meta messages for msg in meta_messages: # Append the message to the meta list for this track track_meta[i].append(msg) # ====================== # Meta-only tracks # ====================== # If this is just a meta track if (len(meta_messages) == len(track)): # We'll first assume the split index is 0 split_index = 0 # If this is not the first track if (i != 0): # Get the indices the previous track was split into previous_track = split_indices[len(split_indices) - 1] # Set the split index as the previous maximum index plus one split_index = previous_track[len(previous_track) - 1] + 1 # Append this index to the list split_indices.append([split_index]) # Add this track index to the list recording which tracks are only meta meta_track_indices.append(len(output_tracks)) # Add sub-list where this track will be stored output_tracks.append([]) # Create a new track finished_track = MidiTrack() # Append this track to the sub-list output_tracks[len(output_tracks) - 1].append(finished_track) # Create a new variable to keep track of skipped delta time skipped_ticks = 0 # Loop through all messages for msg in track_meta[i]: # If this is a tempo message if (msg.type == "set_tempo"): # Increase the skipped time by the amount of this message skipped_ticks += msg.time # Don't append it continue # Increase this message's time by the amount of skipped ticks msg.time += skipped_ticks # Reset the amount of skipped time skipped_ticks = 0 # Append message to the finished track finished_track.append(msg) # Skip everything below continue # =========================== # Convert Note Format # =========================== # Create a variable to store the state of the sustain controller sustain = False # Create a variable to store the state of the sustain controller last loop sustain_last = False # Create a variable to store the state of the sostenuto controller sostenuto = False # Create a variable to store the state of the sostenuto controller last loop sostenuto_last = False # Create a variable to store all notes being sustained by the sostenuto controller sostenuto_notes = [] # Create a variable to turn all notes off off = False # Loop through all notes in the track for j, msg in enumerate(track): # If this is the last message if (j == len(track) - 1): # Turn all notes off off = True # If there is a change in time then add that to the absolute time tick_time += msg.time # If this is a note on message if (msg.type == "note_on"): # If the note has a nonzero velocity if (msg.velocity != 0): # Create a new entry with the format of [TIME ON, TIME OFF, NOTE, VELOCITY, ALIGNED, TRACK INDEX, CHANNEL, PATCH] track_notes[i].append([ tick_time, None, msg.note, msg.velocity, 0, None, msg.channel, get_patch(patch_dictionary, msg.channel, tick_time) ]) # If this channel doesn't already have a patch if (patch_dictionary[msg.channel] == None): # Assume that it is has a default patch of 1 patch_dictionary[msg.channel] = 1 # If the note has a zero velocity else: # Loop through the notes list backwards for j in reversed(range(len(track_notes[i]))): if (msg.note in sostenuto_notes): break # Check if there is a note that is the same note and doesn't end yet if (track_notes[i][j][2] == msg.note and track_notes[i][j][1] == None): # Set the current time as its end time track_notes[i][j][1] = tick_time # If this is a note off message and sustain is not active if (msg.type == "note_off" and not sustain): # If this note is being sustained by sostenuto if (msg.note in sostenuto_notes): # Don't stop it break # Loop through the notes list backwards for j in reversed(range(len(track_notes[i]))): # Check if there is an active note that doesn't end yet if (track_notes[i][j][2] == msg.note and track_notes[i][j][1] == None): # Add the current time as its end time track_notes[i][j][1] = tick_time # If this message is a controller change if (msg.type == "control_change"): # If it is a sustain message if (msg.control == 64): # Set it on/off sustain = not msg.value < 64 # If it is a sostenuto message if (msg.control == 66): # Set it on/off sostenuto = not msg.value < 64 # If this is a controller message to turn all notes off if (msg.control == 120): # Turn on the flag off = True # If this is a reset controllers message if (msg.control == 121): # Turn off sustain and sostenuto sustain = False sostenuto = False # If sostenuto just turned on if (sostenuto and not sostenuto_last): # Remove the notes it was holding down sostenuto_notes.clear() # Check which notes are being pressed notes_on = list(filter(lambda e: e[1] == None, track_notes[i])) # Loop through the notes for note in notes_on: # Add their pitch to the list sostenuto_notes.append(note[2]) # If sustain has changed to off if (sustain_last and not sustain): # Check which notes are on and aren't being sustained by sostenuto notes_on = list( filter( lambda e: e[1] == None and e[2] not in sostenuto_notes, track_notes[i])) # Loop through them for note in notes_on: # Set their end time to now note[1] = tick_time # If sostenuto has been released if (sostenuto_last and not sostenuto): # Loop through all notes for note in track_notes[i]: # Loop through all notes being sustained by sostenuto for sostenuto_note in sostenuto_notes: # If they have the same pitch and the note has no end time if (note[2] == sostenuto_note and note[1] == None): # Set its end time to now note[1] = tick_time # If there is an all notes off message if (off): # Loop through all notes for note in track_notes[i]: # If the note doesn't have an end time if (note[1] == None): # Set its end time to now note[1] = tick_time # Clear all sostenuto notes sostenuto_notes.clear() # Turn off sostenuto sostenuto = False sostenuto_last = False # Turn the all notes off flag off off = False # Set the current value of sustain to the last variable for the next loop sustain_last = sustain # Set the current value of sostenuto to the last variable for the next loop sostenuto_last = sostenuto # ===================== # Note Processing # ===================== # If there are any notes that have the same end and start time(0 duration), delete them track_notes[i] = [ note for note in track_notes[i] if note[0] != note[1] ] # Remove duplicate notes track_notes[i] = [ list(new_note) for new_note in set( tuple(note) for note in track_notes[i]) ] # Convert the note time to second track_notes[i] = notes2second(track_notes[i], tempo_dict, input_song.ticks_per_beat) # Sort the notes by their end time track_notes[i].sort(key=lambda e: e[1]) # Loop through notes for note in track_notes[i]: # Find the minimum time the note must begin/end at min_time = note[1] - Decimal(alignment_margin) # Find the maximum time the note must begin/end at max_time = note[1] + Decimal(alignment_margin) # Create a variable to store the mean time of all overlapping notes mean_time = Decimal(0) # Create a variable to store how many notes are within the margin overlap_notes = 0 # If the note's end has already been aligned if (bool(note[4] & 0b01)): # Skip the rest of the loop continue # Loop through all notes for overlap in track_notes[i]: # If the note starts within the margin and its start has not been aligned if (overlap[0] >= min_time and overlap[0] <= max_time and not bool(overlap[4] & 0b10)): # Add the note's time to the mean time mean_time += overlap[0] # Add one to the note count overlap_notes += 1 # If the note ends within the margin and its end has not been aligned if (overlap[1] >= min_time and overlap[1] <= max_time and not bool(overlap[4] & 0b01)): # Add the note's time to the mean time mean_time += overlap[1] # Add one to the note count overlap_notes += 1 # If is no other note if (overlap_notes < 2): # Skip the rest of the loop continue # Average the mean time mean_time /= overlap_notes # Loop through all notes for overlap in track_notes[i]: # If the note starts within the margin and its start has not been aligned if (overlap[0] >= min_time and overlap[0] <= max_time and not bool(overlap[4] & 0b10)): # Set the note's time to the mean overlap[0] = mean_time # Turn the bit that signifies the start has been aligned on overlap[4] = overlap[4] | 0b10 # If the note ends within the margin and its end has not been aligned if (overlap[1] >= min_time and overlap[1] <= max_time and not bool(overlap[4] & 0b01)): # Set the note's time to the mean overlap[1] = mean_time # Turn the bit that signifies the end has been aligned on overlap[4] = overlap[4] | 0b01 # If we should not normalize the tempo if (normalized_tempo < 0): # Convert the note time back to ticks with the original tempos track_notes[i] = notes2tick(track_notes[i], tempo_dict, input_song.ticks_per_beat) # If we should normalize the tempo else: # Convert the note time back to ticks with a single tempo track_notes[i] = notes2tick(track_notes[i], {0: normalized_tempo}, input_song.ticks_per_beat) # Only keep notes that do not have the same start and end time(nonzero duration) track_notes[i] = [ note for note in track_notes[i] if note[0] != note[1] ] # Sort the notes by their start time track_notes[i].sort(key=lambda e: e[0]) # ====================== # Track Splitting # ====================== # Create a new list for new(split) tracks new_tracks = [] # Iterate through all notes in the current track for note in track_notes[i]: # Calculate the track index of the note track_index = get_track_index(note, track_notes[i]) # If we need more tracks, add them for j in range(1 + track_index - len(new_tracks)): new_tracks.append([]) # Add the note to its track new_tracks[track_index].append(note) # ================== # Indexing # ================== # Create a new variable to store the starting index initial_index = 0 # Loop through all split tracks in the output_tracks list for split_tracks in output_tracks: # Increase the starting index by the amount of split tracks initial_index += len(split_tracks) # If we are indexing patches if (index_patches): # For every new track for j, new_track in enumerate(new_tracks): # Loop through the notes for note in new_track: # Set the patch to a clamped index value between 0 and 127 note[7] = min(max(0, initial_index + j), 127) # ====================== # Track Output # ====================== # Add parent list where split tracks from this track will be stored output_tracks.append([]) for j, new_track in enumerate(new_tracks): # Create a new track to append to the MIDI file that will be exported finished_track = MidiTrack() # Use tick_time to represent absolute time and set it to 0 tick_time = 0 # Set the finished track name to the old track name concatenated with an index starting with 1 finished_track.name = track.name + " " + str(j + 1) # Loop through all of the notes in the new track for note in new_track: # Add a note_on message for the note finished_track.append( Message("note_on", note=note[2], velocity=new_velocity if new_velocity >= 0 else note[3], time=(note[0] - tick_time), channel=note[6])) # Add a note_off message for the note finished_track.append( Message("note_off", note=note[2], velocity=0, time=(note[1] - note[0]), channel=note[6])) # Set the absolute time counter to the last message added(the note_off message) tick_time = note[1] # Use tick_time to represent absolute time and set it to 0 tick_time = 0 # Create a variable to store the previous patch last_patch = None # If we are creating channels and not indexing patches if (create_channels and not index_patches): # We can now reasonably make the assumption that each channel has a one-to-one correspondence with each track # Loop through all messages for k, msg in enumerate(finished_track): # Update tick_time tick_time += msg.time # If this is a meta message if (msg.is_meta): # Skip this loop iteration continue # If the patch has changed if (not get_patch(patch_dictionary, msg.channel, tick_time) == last_patch): # Insert a patch change message finished_track.insert( k, Message( "program_change", channel=msg.channel, program=get_patch(patch_dictionary, msg.channel, tick_time), time=(tick_time - get_patch_time(patch_dictionary, msg.channel, tick_time)))) # Update the preceding note's time msg.time -= (tick_time - get_patch_time( patch_dictionary, msg.channel, tick_time)) # Update last_patch last_patch = get_patch(patch_dictionary, msg.channel, tick_time) # Append the finished track to the list of tracks to be output output_tracks[i].append(finished_track) # We'll first assume the starting index is 0 starting_index = 0 # If this is not the first track if (i != 0): # Get the indices the previous track was split into previous_track = split_indices[len(split_indices) - 1] # Set the starting index as the previous maximum index plus one starting_index = previous_track[len(previous_track) - 1] + 1 # Append a sub-list to store the indices this track is split into split_indices.append([]) # Loop through the number of tracks this track will be split into for j in range(len(output_tracks[i])): # Add the indices to the sub-list split_indices[i].append(starting_index + j) # ======================= # Song Processing # ======================= # If we are collating the output if (collated): # Create a variable to store the maximum number of times a track was split max_split = 0 # Loop through all tracks for split_track in output_tracks: # If this track was split more times, make it the new maximum max_split = max(max_split, len(split_track)) # Loop through the sub-lists for i in range(max_split): # Loop through the initial(outer) lists for split_track in output_tracks: # If we have already appended all split tracks on this track if (i >= len(split_track)): # Skip this loop iteration continue # Add track to output file output_song.tracks.append(split_track[i]) # If the tracks should not be collated else: # Flatten the list of output tracks output_tracks = [ output_track for split_track in output_tracks for output_track in split_track ] # Loop through tracks for track in output_tracks: # Add track to output file output_song.tracks.append(track) # If we should normalize the tempo if (normalized_tempo > 0): # Set the tempo at the beginning of the song output_song.tracks[0].insert( 0, MetaMessage("set_tempo", tempo=normalized_tempo)) # If we are not normalizing the tempo else: # If there are no meta only tracks if (len(meta_track_indices) == 0): # Create a new track output_song.tracks.insert(0, MidiTrack()) # Add its index to the list of meta only tracks meta_track_indices.append(0) # Create a list to store tempos in tempos = [] # Convert tempo_dict to a 2d list for time in tempo_dict: tempos.append([time, tempo_dict[time]]) # Re-use tick_time to store absolute time tick_time = 0 # Create a variable to store the index of the last tempo inserted tempo_index = 0 # Create a variable to store the total length of the track total_time = 0 # Get the length of the track for msg in output_song.tracks[meta_track_indices[0]]: total_time += msg.time # Create a new variable to store if the first meta track is empty first_meta_empty = len(output_song.tracks[meta_track_indices[0]]) == 0 # Loop through the first meta track to which we will add tempo messages(which will be the original length + # of tempo messages when done) for i in range( len(output_song.tracks[meta_track_indices[0]]) + len(tempos)): # If we addded all the tempos if (tempo_index == len(tempos)): # Stop adding them break # If the meta track is/was empty if (first_meta_empty): # Insert the message output_song.tracks[meta_track_indices[0]].append( MetaMessage("set_tempo", tempo=tempos[tempo_index][1], time=tempos[tempo_index][0] - tick_time)) # Increment tick_time tick_time += output_song.tracks[meta_track_indices[0]][i].time # Increment the tempo index tempo_index += 1 # Skip everything below continue # Increment tick_time tick_time += output_song.tracks[meta_track_indices[0]][i].time # If we're at the end of the list if (i == len(output_song.tracks[meta_track_indices[0]]) - 1): # Insert the message output_song.tracks[meta_track_indices[0]].append( MetaMessage("set_tempo", tempo=tempos[tempo_index][1], time=tempos[tempo_index][0] - tick_time)) # Increment the tempo index tempo_index += 1 # Skip everything below continue # If the tempo is between these messages if (tick_time <= tempos[tempo_index][0] and tempos[tempo_index][0] <= tick_time + output_song.tracks[meta_track_indices[0]][i + 1].time): # Decrease the delta of the message after it output_song.tracks[meta_track_indices[0]][ i + 1].time -= tempos[tempo_index][0] - tick_time # Insert the message output_song.tracks[meta_track_indices[0]].insert( i + 1, MetaMessage("set_tempo", tempo=tempos[tempo_index][1], time=tempos[tempo_index][0] - tick_time)) # Increment the tempo index tempo_index += 1 # If we didn't add all the tempos if (tempo_index < len(tempos) - 1): # Loop through the remaining tempos for i in range(tempo_index, len(tempos)): # Append the remaining messages output_song.tracks[meta_track_indices[0]].append( MetaMessage("set_tempo", tempo=tempos[i][1], time=tempos[i][0] - tick_time)) # Set tick_time tick_time = tempos[i][0] # If we are assigning patches on the output tracks if (index_patches): # Loop through all the tracks for i, track in enumerate(output_song.tracks): # Start off with a patch index of the current track index patch_index = i # For every meta only track before it, subtract one for index in meta_track_indices: if index < i: patch_index -= 1 # Set the patch of each of the track output_song.tracks[i] = set_track_patch(track, patch_index, patch_index) # If we are creating output channels if (create_channels): # Loop through all the tracks for i, track in enumerate(output_song.tracks): # Start off with a channel index of the current track index channel_index = i # For every meta only track before it, subtract one for index in meta_track_indices: if index < i: channel_index -= 1 # Set the channel of each of the tracks output_song.tracks[i] = set_channel(track, channel_index) # ==================== # Song Output # ==================== # Try to save the song try: # Save the song output_song.save(output_file) # If it saves, return true return True except Exception as err: # Print the error traceback.print_tb(err.__traceback__) # If it fails to save, return an exception return Exception( "Could not save file, try checking the output file path")