Music with Markov Chains

This is a quick tutorial showing how to make music using Markov chains in Python. (If you’re not familiar with Markov chains, this page gives a great, interactive introduction.) The general idea is that we can input melodies of a certain style, and our model will output similar melodies that reflect that style. We’ll use a sequence of integers representing notes in a melody to train our model (i.e. build a transition table), and then compare the output.

First, start up Python and import the following libraries:

import numpy as np
import pandas as pd
import random

Next, we need melodies to use as input to train our model. You can compose them yourself, transcribe them, or use a dedicated toolkit like music21 to extract them from digital score files. Here I’ll just use the first few notes of a couple of familiar melodies. The integers are MIDI note numbers (transposed to C major):

brother_john = [60, 62, 64, 60, 60, 62, 64, 60]
little_lamb = [64, 62, 60, 62, 64, 64, 64]

We want to keep the melodies in separate lists to avoid introducing “false” patterns that never actually occur in the melody between the last note of one melody and the first note of the next. For example, the last note of the first melody is 60 and the first note of the second melody is 64. But the pattern 60 64 never occurs in either melody, so we keep them separate!

A Markov chain can be represented through what’s known as a transition table. The transition table specifies how likely we are to move from one state to another. This function will build our transition table:

def make_table(allSeq):
 n = max([ max(s) for s in allSeq ]) + 1
 arr = np.zeros((n,n), dtype=int)
 for s in allSeq:
  ind = (s[1:], s[:-1])
  arr[ind] += 1
 return pd.DataFrame(arr).rename_axis(index='Next', columns='Current')

Then we call the function with whatever melodies we’d like to include as items in a list (any number of melodies). This builds the transition table, essentially “training” the model:

transitions = make_table([brother_john, little_lamb])

The next step is to generate a new sequence based on the table. So we need a new function:

def make_chain(t_m, start_term, n): # trans_table, start_state, num_steps
 chain = [start_term]
 for i in range(n-1):
  chain.append(get_next_term(t_m[chain[-1]]))
 return chain

Inside of which we use the following nested function for each step:

def get_next_term(t_s):
 return random.choices(t_s.index, t_s)[0]

And finally, we’re ready to create our chain by calling the function using three arguments (transition table name, starting value, and length of sequence):

make_chain(transitions, 60, 10)

And we get something like this:

>>> [60, 60, 62, 64, 60, 62, 64, 60, 60, 60]

Try it a few times to see what kind of results you get. Switch up the starting value and train it on more melodies! Have fun!