I have written a program that takes in a MIDI file, then using Mido, it cleans the data by removing particular types of meta data, repeat messages, etc. It also calculated the cumulative time (as the time in each midi message is in delta time). This is then used to create a new mido file (from scratch) where I append all of these messages into one track (so essentially the tracks are merged)and sort them by cumulative time. The delta time is then adjusted accordingly (keeping in mind that each new midi track starts from cumulative time 0). I realise this may seem pointless (as I am trying to construct the same song just cleaner), however the purpose is to have nicer data to then do other stuff with.
I have split my code into two parts. The first does all the filtering and constructs a big list of lists, where the first item in each sub-list is the message itself and second item in each sub-list is the cumulative time (and this is sorted by cumulative time as stated above). The second part of the code adjusts the delta time of each item in this list and then appends all the messages from the list in order (with the corrected delta times) onto a track created from scratch. It then plays this track using pygame.
The major problem I seem to be encountering is the timing/tempo. The reconstructed tracks either seem to be playing too fast or too slow. In the case of some files (for example the Bohemian Rhapsody file) the instrumental parts also seem to be detached and muddled.
This is the deconstructing and list building code:
import mido
import pygame
all_mid = ['major-scale.mid']
# check is midi file is type 2 (and removes if so) - this is unlikely but can happen on old sites
def remove_type_2(midi):
return True if midi.type == 2 else False
# removes unnecessary meta data types
def filter_meta_type(msg):
accept = ["set_tempo", "time_signature", "key_signature"]
return True if msg.type in accept else False
# removes tempo duplicates and only keeps the last tempo stated for a particular cumulative time
def remove_extra_tempo(msg, msgwithtempos, current_time):
if not msgwithtempos: # if the list is empty
msgwithtempos.append([msg, current_time])
else:
for i in range(len(msgwithtempos)):
msgwithtempo = msgwithtempos[i]
if msgwithtempo[1] == current_time: # this checks duplicates
msgwithtempos.remove(msgwithtempo)
msgwithtempos.append([msg, current_time])
return msgwithtempos
def do_shit(mid, all_messages): # for each track (then message) do the following
msgwithtempos = []
for i, track in enumerate(mid.tracks):
current_time = 0
print(f"Track {i}: {track.name}")
for msg in track:
current_time += msg.time
if msg.type == "sysex data":
pass
elif msg.is_meta:
if filter_meta_type(msg):
if msg.type == "set_tempo":
msgwithtempos = remove_extra_tempo(msg, msgwithtempos, current_time)
else:
all_messages.append([msg, current_time])
else:
all_messages.append([msg, current_time])
return all_messages, msgwithtempos
def main(): # for each midi file do the following
all_lists = []
for i in range(0, len(all_mid)):
all_messages = []
mid = mido.MidiFile(all_mid[i])
if not remove_type_2(mid):
all_messages, msgwithtempos = do_shit(mid, all_messages)
final_messages = all_messages + msgwithtempos
final_messages = sorted(final_messages, key=lambda x: x[1])
all_lists.append(final_messages)
print(all_lists)
return all_lists
if __name__ == '__main__':
main()
This is the reconstructing code:
import mido
import pygame
import regulate_tracks
def play_with_pygame(song):
pygame.init()
pygame.mixer.music.load(song)
length = pygame.time.get_ticks()
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.wait(length)
def printmessages(mid):
for i, track in enumerate(mid.tracks):
print(f"Track {i}: {track.name}")
for msg in track:
print(msg)
def main():
# get the list of midi files from regulate_tracks
output = regulate_tracks.main()
list1 = output[0]
# create a blank midi file and add a track to it
mid = mido.MidiFile()
track = mido.MidiTrack()
mid.tracks.append(track)
for i in range(len(list1)):
message = list1[i][0]
print(message.type)
if i == 0:
message.time = 0
else:
message.time = list1[i][1] - list1[i - 1][1]
print(message)
track.append(message)
mid.save('new_song.mid')
printmessages(mid)
play_with_pygame('new_song.mid')
if __name__ == '__main__':
main()
Example files:
Bohemian Rhapsody: https://bitmidi.com/queen-bohemian-rhapsody-mid
River Flows In You: http://midicollection.net/songs/index.php?id=13
Thanks
edit:
It seems to be a problem with the blank midi creation and appending as I created this code that manually copied messages from a short midi files and they were still sounded wrong (slower) when played.
import mido
import pygame
def play_with_pygame(song):
pygame.init()
pygame.mixer.music.load(song)
length = pygame.time.get_ticks()
pygame.mixer.music.play()
while pygame.mixer.music.get_busy():
pygame.time.wait(length)
def main():
mid = mido.MidiFile()
track = mido.MidiTrack()
mid.tracks.append(track)
track.append(mido.MetaMessage('set_tempo', tempo=500000, time=3840))
track.append(mido.MetaMessage('end_of_track', time=0))
track = mido.MidiTrack()
mid.tracks.append(track)
track.append(mido.Message('note_on', channel=0, note=60, velocity=100, time=0))
track.append(mido.Message('note_on', channel=0, note=62, velocity=100, time=960))
track.append(mido.Message('note_on', channel=0, note=64, velocity=100, time=960))
track.append(mido.Message('note_on', channel=0, note=65, velocity=100, time=960))
track.append(mido.Message('program_change', channel=0, program=123, time=960))
track.append(mido.Message('note_on', channel=0, note=67, velocity=100, time=0))
track.append(mido.Message('note_on', channel=0, note=69, velocity=100, time=960))
track.append(mido.Message('note_on', channel=0, note=71, velocity=100, time=960))
track.append(mido.Message('note_on', channel=0, note=72, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=60, velocity=100, time=2880))
track.append(mido.Message('note_off', channel=0, note=62, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=64, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=65, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=67, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=69, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=71, velocity=100, time=960))
track.append(mido.Message('note_off', channel=0, note=72, velocity=100, time=960))
track.append(mido.MetaMessage('end_of_track', time=0))
mid.save('new_song.mid')
play_with_pygame('new_song.mid')
if __name__ == '__main__':
main()
You need to save the ticks_per_beat
. Simply get the ticks_per_beat
of the original file with ticksperbeat = mid.ticks_per_beat
, then set the new file to have mid.ticks_per_beat = ticksperbeat
.