Week6 - MIDI Blues

November 10, 2018

Parsing the MIDI files

For my second performance i am trying to generate a music based on some Persian MIDI files i downloaded from various places. The issue with these files is that they’re all over the place and don’t necessarily follow a coherent logic.

Trying to parse each file into its building components seem futile.

So what i ended up doing after hours of playing with ableton Live, Music21 library, and Mido library, was to open each file in MuseScore, find the instrument i want and isolate it into a single MIDI file and save it.

Normal MIDI file with all its instruments in MuseScore

Midi with only Drums seperated in MuseScore

Encoding MIDI into custom phrases for LSTM

So the goal is to use Natural Langauge processing training tools like LSTM to train on our MIDI files. But for that we need to turn MIDI into a “language”. That means turning each MIDI message like channel=10 note=31 velocity=124 time=0 into a “phrase” like C10N31V124T0 so we can treat it as words.

For now, i am just trying to isolate and analyze the beats for each file.

So normally, if i use mido library in python to extract the MIDI messages in my file, it would look like this:

from mido import MidiFile

mid = MidiFile('song.mid')

for i, track in enumerate(mid.tracks):
    print('Track {}: {}'.format(i, track.name))
    for msg in track:
        print(msg)

and part of the output shows us how mido looks at the MIDI files:

note_on channel=11 note=60 velocity=0 time=0
control_change channel=10 control=84 value=39 time=0
note_on channel=10 note=31 velocity=124 time=0
note_on channel=10 note=39 velocity=0 time=0
<meta message sequencer_specific data=(67, 123, 1, 53, 8, 53, 8) time=0>
note_on channel=11 note=67 velocity=0 time=36
note_on channel=11 note=74 velocity=0 time=4
note_on channel=11 note=70 velocity=0 time=4

in here, note_on is not part of the message, but the message type, another type would be note_off , or control_change

None of my MIDI files actually contain note_off, instead they use velocity=0 to turn the note off.

after i seperate the Drum notes from the rest, my messages will look like this:

note_on channel=9 note=64 velocity=90 time=0
note_on channel=9 note=54 velocity=44 time=0
note_on channel=9 note=64 velocity=0 time=359
note_on channel=9 note=54 velocity=0 time=0
note_on channel=9 note=63 velocity=119 time=1

I have no idea what other types of messages are doing, and only thing i care about my MIDI notes. This tells me that all drum tracks are on the 10th Channel channel=9 and are turned on and off with velocity . So i only need Note, Velocity, and Time for my encoding. So my desired format would be like:

N##V##T##

I only care about the note_on message type. In order to seperate them out i will use this statement:

notes = []
for message in mid.tracks[0]:
    if message.type == 'note_on':
        message_components = str(message).split(' ')

The last line will convert my messages into a string type, splits them where there’s a space, and puts it in a list for me:

['note_on', 'channel=9', 'note=64', 'velocity=0', 'time=479']

now we iterate through each item, find note=, velocity=, and time=, get the number after = and build the N##V##T## phrase with it:

phrase = "" # create an empty phrase, we do it here for scope

        for item in message_components: # note=0,velocity=0, etc. are SEPARATE ITEMS in the message_component list


            if 'note=' in item: 

                phrase = "" # this just empties my phrase after it finds the next "note=##" 

                value = str(item).split('=') # convert item from list to string, then split that over '=' 
                                            #which in turn creates another list 

                value = str(value[1]) # convert the SECOND item on the list to string and store it
                value = 'N' + value 

                phrase = phrase + value


            if 'velocity=' in item:

                value = str(item).split('=') # convert item from list to string, 
                                            #then split that over '=' which in turn creates another list 

                value = str(value[1]) # convert the SECOND item on the list to string and store it
                value = 'V' + value 
                
                phrase = phrase + value

            if 'time=' in item:

                value = str(item).split('=') # convert item from list to string, 
                                            #then split that over '=' which in turn creates another list 

                value = str(value[1]) # convert the SECOND item on the list to string and store it
                value = 'T' + value 
                
                phrase = phrase + value

                notes.append(phrase) #adds the fully constructed N#V#T# phrase to the notes[] list as a new item

Sorry if it’s too commenty!

anyways, in the end we want our encoding in text format, with a space between each phrase. so:

notes = ' '.join(notes) #join will join all the items in notes[] list into a string, and puts a ' ' between them. 
print(notes)

i print them in the end so i could check if everything went smoothly.

Anyways, after saving the file we will end up with something like this:

N60V0T47 N63V127T1 N63V0T119 N63V105T1 N63V0T239 N64V127T1 N54V49T0

which is ready to be fed into LSTM. But first we have to make sure it can be turned back into an OK sounding MIDI

Decoding the custom phrase back to MIDI

Now the “prestige” part of the trick, is to see if we can turn it back.

First, we have to extract the numbers out from our phrases. For that i will use the regular expression library in python with import re.

import re

text = open('notes.txt', 'r')
text = text.read() #reads the txt file into a string

numbers = re.findall('\d+', text) # '\d+' extracts all the numbers in 'text' and puts them in a list

if i used \d instead of \d+ it would give only the FIRST integer it find. the + makes it into “This integer and the ones next to it”. This way the integers in N3 and N54 and N120 will all be extracted correctly.

Of course there are multiple way to build a MIDI file. Like using the midiutil library in python. But i found mido to still be easier to understand.

from mido import Message, MidiFile, MidiTrack

mid = MidiFile()
track = MidiTrack()
mid.tracks.append(track)

def chunks(l, n): 
    """Yield successive n-sized chunks from l."""
    for i in range(0, len(l), n):
        yield l[i:i + n]

track.append(Message('program_change', program=12, time=0)) #MIDO docs say this is needed as the first message. No idea what it does. 

midi_notes = list(chunks(numbers, 3))
for midi_note in midi_notes:
    track.append(Message('note_on', note=int(midi_note[0]), velocity=int(midi_note[1]), time=int(midi_note[2])))

    print('{}_{}_{}'.format(midi_note[0], midi_note[1], midi_note[2])) #print to see where there are errors in encoding. 
comments powered by Disqus