This module will show you how to make music using Markov chains.
If you’re not familiar with Markov chains, this page gives a great, interactive introduction. The basic idea is that we can input melodies of a certain style, and our model will output similar melodies that reflect that style.
First, start up Python and import the following libraries (you must install each one first if you haven’t already):
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, transcribe them, or extract them from digital score files. In this module, we’re representing the melodies using MIDI note numbers.
Let’s use the first few notes of a couple of familiar melodies:
brother_john = [60, 62, 64, 60, 60, 62, 64, 60] little_lamb = [64, 62, 60, 62, 64, 64, 64]
The “path” of a Markov chain is defined by something called a transition table. The transition table specifies how likely we are to move from one state to another–or in this case, one note to another.
This function takes our input melodies and builds our transition table, using some special features of the numpy and pandas libraries:
def make_table(all_melodies): t_size = max([ max(melody) for melody in all_melodies ]) + 1 t_table = np.zeros((t_size,t_size), dtype=int) for melody in all_melodies: coords = (melody[1:], melody[:-1]) t_table[coords] += 1 return pd.DataFrame(t_table).rename_axis(index='Next', columns='Current')
First we define the size of the transition table based on the highest value (pitch) in any of the melodies (using a list comprehension to build the list from which we pull the maximum value). We add + 1 since we’re counting from zero. The next line builds an empty table of specified size using the numpy function np.zeros.
Then we run a for loop to populate the table based on transitions between notes in the melodies. The “current” note is given on the horizontal axis and the “next” note is given on the vertical axis. Any transition that occurs in a melody adds 1 to the value of the cell where the “current” column and “next” row intersect. Finally, the function outputs the table, but with “Next” and “Current” labels added.
With the function complete, we can now call it with whatever melodies we’d like to use to train the model as items in a list:
transitions = make_table([brother_john, little_lamb])
You can use any number of melodies–even one (just make sure to still enclose it within square brackets).
Now that we have a transition table, we can use it to generate a new sequence with another function:
def make_chain(t_table, start_state, num_steps): chain = [start_state] for i in range(num_steps - 1): chain.append(get_next_state(t_table[chain[-1]])) return chain
In this function we start building the Markov chain with the starting state. Then for each step (in the number of steps specified), we add a step to the chain using a nested function, and finally return the whole chain.
The nested function uses the random.choices function from the random library, which lets us set weights for random picking from the transition table. The input to this function is the probabilities from the transition table for the last item in the chain (accessed using index [-1]).
def get_next_state(weights): return random.choices(weights.index, weights)
And finally, we’re ready to generate our chain by calling the function using three arguments (transition table name, starting value, and length of sequence):
make_chain(transitions, 60, 10) > [60, 60, 62, 64, 60, 62, 64, 60, 60, 60]
Try it a few times to see what kind of results you get, or try changing the starting value. Train it on more melodies–your own, or someone else’s!
- Why is it inadvisable to flatten all training melodies into a single list of notes (i.e. a single melody)?
- How can you convert the list of integers back into musical notation using music21? (Hint: turn it into a stream.)