Let’s Program Some Weird Markov Chains Part 6: Listening To Music Reeeeaaaally Closely

Last time we learned how to represent musical chords with a Markov Chain as well as how to use them to programmatically write music. Unfortunately the music sounds really really bad because I have no idea what chords are supposed to sound like or what chords are supposed to follow each other.

That’s why this time we’re going to focus on reading files instead of writing them. That way our program can learn from actual musicians what notes and chords should follow each other. Because I’m certainly never going to figure that stuff out.

Of course, my lack of musical knowledge also limits my ability to write code that analyzes existing music but if I can get even close we should see some interesting results. And then all you out in the audience can do me one better by learning enough about real music and mp3 to write something better than the upcoming prototype.

Let’s start by importing some libraries, writing a couple helper functions and setting up a variable to hold our musical markov chain data.

from mido import Message, MidiFile, MidiTrack
from random import randint
       
def getRandomFromProbabilityList(probabilityList):
    total = 0
    for word, count in probabilityList.items():
        total = total + count
    chosenProbability = randint(0, total -1)
    for word, count in probabilityList.items():
        if(chosenProbability < count):
            return word
        chosenProbability = chosenProbability - count
        
def addChordToTrack(track, musicalTuple):
    track.append(Message('note_on', note=musicalTuple[0], velocity=64, time=500))
    for subNote in musicalTuple[1:]:
        track.append(Message('note_on', note=subNote, velocity=64, time=0))
 
markovChain = {}

As you can see we’ve got our classic function for pulling an option out of a list of weighted options as well as a simple function for taking a set of musical notes and adding them to a midi track as a half second long chord.

Next we need a lot of raw music. I found a nice collection of publicly available midis of public domain classical music. I downloaded several from the piano section, put them in a folder named “piano” and then threw their file names into a big list in the code.

inputFiles = ['piano/alice.mid',
                'piano/alkan-op31-1.mid',
                'piano/andre-sonatine.mid',
                'piano/anna-magdalena-20a.mid',
                'piano/bach-invention-01.mid',
                'piano/bach-invention-07.mid',
                'piano/bach-invention-13.mid',
                'piano/bach-invention-14.mid',
                'piano/bluemtns.mid',
                'piano/bwv_462.mid',
                'piano/BWV-117a.mid',
                'piano/bwv-259.mid',
                'piano/bwv347.mid',
                'piano/BWV-511.mid',
                'piano/cpe-bach-rondo.mid',
                'piano/giselle.mid',
                'piano/lanative.mid',
                'piano/lesgraces.mid',
                'piano/Rumores_de_la-caleta.mid',
                'piano/swallows.mid',]

And then we can loop through all the files, use our midi library to loop through all the chords in the piece and build up a one deep markov chain from them.

Getting the chords from a midi file is a little bit more difficult than getting the words from a book. As we saw last time a midi chord is created by using “time = 0” to make several notes play at the same time as the note before them. So to build a cord we loop through “note_on” type events. If the time is 0 we add it to the current chordTemp item. If the time isn’t 0 it must be the start of a new chord so we push the current chord onto our main list of chords, clear it out, add the new note and then start looping again.

And of course, once we have our big list of every chord and the order it happened in we can build a one deep markov chain just the same we did for words.

for inputFile in inputFiles:
 
    mid = MidiFile(inputFile)

    chordList = []
    chordTemp = []

    for msg in mid:
        info = msg.dict()
        if(info['type'] == 'note_on'):
            if(msg.time != 0):
                chordList.append(tuple(sorted(set(chordTemp))))
                chordTemp=[]
            chordTemp.append(msg.note)
                


    for i in range(len(chordList) - 1):
        if chordList[i] not in markovChain:
            markovChain[chordList[i]] = {}
        if chordList[i+1] not in markovChain[chordList[i]]:
            markovChain[chordList[i]][chordList[i+1]] = 1
        else:
            markovChain[chordList[i]][chordList[i+1]] += 1

The last thing we need is a list of starting points for generating new songs. Once again this is a little tricky. A book has thousands upon thousands of senteces each with an easy to find first word. But music? Much harder to pick out the start of a musical phrase within a midi.

So we’ll just take a shortcut and make a list of every chord in our markov chain that links to at least one other chord. And then we’ll grab one at random to be the start of our song.

possibleStartingChords = list(markovChain.keys())
startingChord = possibleStartingChords[randint(0,len(possibleStartingChords)-1)]       

And from there building a new song is easy peasy. Only difference from working with sentences is that our goal here is to produce a song of a certain length instead of just running until we reach a data point with no next possible value. So we loop through 480 chords, using the markov chain whenever we can and choosing a new random note to continue with everytime we hit a dead end.

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

track.append(Message('program_change', program=0, time=0))
            
currentChord = startingChord

for x in range(480):
    if(currentChord in markovChain):
        currentChord = getRandomFromProbabilityList(markovChain[currentChord])
    else:
       currentChord = possibleStartingChords[randint(0,len(possibleStartingChords)-1)] 
    addChordToTrack(track, currentChord)

newMid.save('markov_song.mid')

Run it a few times and give the results a listen. What do you think?

Awful isn’t it?

But awful in a way that at times sounds almost like music.

I think the main problem is that several of the chords don’t sound very good. This is probably because our naïve chord collector doesn’t distinguish between different instruments. Nor does it have any way to distinguish between treble and bass cleft chords that happen to be being played at the same time.

This is going to make me sound pretty lazy, but I’m happy leaving this experiment where it is. It’s not a great program for generating music… but as a proof of concept program written by someone who barely understands music it’s pretty good. Or at least, it’s good enough that another programmer who actually understands music could probably take it, improve and make something cool.

So we’ll call this a sort-of success and next time we’ll move on to our final and least likely to succeed experiment: Writing a Markov chain system for painting pictures..