Search code examples
pythonmidimido

Why is clean Midi file playing differently? (with mido)


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()

Solution

  • 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.