How to generate music with Python: The Basics

Steve Hiehn
10 min readAug 16, 2022

--

For several years I have been using python to procedurally generate midi, which has dramatically sped up content creation for my music authoring software, Signals&Sorcery. With a little guidance generating music with code is accessible and fun. It offers the potential to create interactive music and can automate tedious workflows like generating loop and sample variations. I created this practical guide to provide just enough music theory and technical knowledge to get started procedurally generating music. Not gonna lie, both coding and music are quite involved disciplines. I’ve been making music for a solid 25 yrs and coding for about 15 yrs. I’m going to do my best to cut any fluff and give you just the knowledge you need to get started. By the end of this article, you should be able to generate midi basslines, chords & arpeggios which sound good & adhere to a musical chord progression. Not only that, but you will also be able to import the MIDI into your favorite DAW (i.e Ableton) and export your own banger! ;)

Let’s listen to the final result:

python-midi-tutorial.mp3

I want to start with a disclaimer: Tonal harmony rules are not really rules, they are guidelines and formulas to create certain sounds. The only real rule in music is if it sounds good, it is good. So, with that out of the way, let’s start with some music basics.

SCALE: A music scale is a collections of notes that all sound ‘correct’ together. It’s the music equivalent of a visual color palette. If you are designing a website you might limit the colors to: dark blue, light blue, crimson & white. Just because you use one of those colors does not necessarily mean it’s going to look good, but at least it will be on theme which helps.

Arguably the most common scale in western music is C major, AKA the white keys on the piano. There are thousands upon thousands of amazing pieces of music which only use the notes in C major and millions more if you start to include all the other scales. But, for this tutorial, we will limit ourselves to the C major scale, but these concepts apply to any scale.

CHORD: A chord is three or more notes played at the same time. I’m sure many people have strummed a chord on a guitar. In fact if you don’t even push any notes on the fretboard and strum all the strings you are playing a chord. Maybe not a pleasant one, but a chord nonetheless.

ARPEGGIO: An arpeggio is pretty much the same as a chord but the notes are played in sequence opposed to playing them concurrently.

BASSLINE: Basslines are low notes which usually follow the chord tones. In fact the majority of basslines just play the root note of the chord. So, if the chord in question is C major7, then the root note would be a C. Many effective basslines are simply root notes. However does not have to be the case. It’s not uncommon in some styles of music (funk or jazz) to have bassline be even more complex than the other instrument ranges.

CHORD PROGRESSION: A chord progression is a sequence of chords. For example, a simple tune might have a chord sequence like C to C to F than G, and then repeat. We’ll use this progression for the examples throughout the rest of the tutorial.

MELODY: Melodies are sequences of aesthetically pleasing notes which harmonize with the underlying chord progression. Melody generation is a very advanced topic and beyond the scope of this tutorial.

LET’S MAKE A CHORD PROGRESSION!

In order to programmatically generate some good sounding arpeggios, chords, and basslines we need a chord progression to follow. There are rules which tell you which chords belong to particular scales. This topic can be quite involved and is the heaviest bit of music theory I’ll drop on you today. I recommend taking this info at face value for the sake of this tutorial but eventually you will likely want to research the relationship between chords and scales further.

In western harmony, a major scale has seven notes. The notes then repeat at the eighth step (called an octave). Sticking with the notes in the key of C Major AKA the white keys on the piano we have:

C — D — E — F — G — A — B

In music theory, each step is traditionally represented by a roman numeral i.e:

I — II — III — IV — V — VI — VII

Each step is assigned a chord quality like so:

I   = Major7 
II = Minor7
III = Minor7
IV = Major7
V = Dominant7
VI = Minor7
VII = Minor7b5

So, if we want to write a chord progression that is strictly in the key of C major the chords you can pick from are:

I   - C maj7
II - D min7
III - E min7
IV - F maj7
V - G Dom7
VI - A min7
VII - B min7b5

What’s pretty cool is you can pick any simple sequence of these chords without even hearing it and it will sound correct

(advance fyi: you can change keys as much as you like, but harmony becomes more complex)

For this tutorial we are going to use [Cmaj7, Cmaj7, Fmaj7, GDom7] then repeat. For each chord listed, it will span the duration of 4 beats. So, 8 beats of the Cmaj7 chord, 4 beats of Fmaj7 followed by 4 beats of Gdom7.

Okay! If you are still reading than congratulations! We have passed the boring stuff and now have enough music theory to start making music with code. There are many libraries and frameworks for music theory and midi generation in many different programming languages. Please use whatever langs & libs that work for your situation. However, for this tutorial we are going to use Python3 as our programing language and 2 libraries:

Mingus a python lib a music theory utility to help us find the right notes, chords and scales to use.

MIDIUtil a python lib which will turn our note names and chords into a actual MIDI that can make noise in a DAW like Ableton

First things first: Make sure you have Python3 installed. If you run into issues, google is your friend, but on a mac it should be as simple as running this command from your terminal (if you don’t have brew installed grab it here: BREW):

brew install python

Lets add both of the mentioned libraries:

pip install mingus

and …

pip install MIDIUtil

Awesome. I mentioned earlier that the definition of a chord is three or more notes at the same time. How do we figure out which notes belong to a chord?Well, thats where mingus comes into play. It can tell us which notes are in each chord with just a little bit of code:

from mingus.core import chords

result = chords.from_shorthand("Cmaj7")

print(result)

which prints:

['C', 'E', 'G', 'B']

What that means is we have an array of notes that all sound good with the chord C Major7. The reason each of those notes sound good or correct with the chord is because the notes are in the chord itself! For example if someone were to strum the chord C Major7 on a guitar and a singer sang (or another instrument played) any of these notes (let’s pickG)it will harmonize and sound correct! Hopefully you are already getting some insight how tonal music is composed.

Now, lets create an array of our chord progression (Cmaj7-Cmaj7-Fmaj7-Gdom7) and loop through the array and print all the notes in the progression one at a time, like so:

from mingus.core import chords

chord_progression = ["Cmaj7", "Cmaj7", "Fmaj7", "Gdom7"]

array_of_notes = []
for chord in chord_progression:
array_of_notes.extend(chords.from_shorthand(chord))

print(array_of_notes)

this code prints:

['C', 'E', 'G', 'B', 'C', 'E', 'G', 'B', 'F', 'A', 'C', 'E', 'G', 'B', 'D', 'F']

The first eight notes make up the definition of Cmaj7, the next 4 make up Fmaj7 and as you might expect the last 4 notes make up the chord Gdom7.

Alright, before we go much further I think it’s important to actually hear what all this sounds like. Thats where MIDI comes in. “Musical Instrument Digital Interface” is a very established compact protocol invented in 1981 and has been relatively unchanged since. Conceptually it’s very simple. It does not make sound, it simply tells a keyboard or instrument what note to play, when to play it and how loud to play it. If you’ve ever seen a historic player piano than you have pretty much seen the non-digital version of MIDI. Let’s create midi from our notes now:

from midiutil import MIDIFile
from mingus.core import chords

chord_progression = ["Cmaj7", "Cmaj7", "Fmaj7", "Gdom7"]

NOTES = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
OCTAVES = list(range(11))
NOTES_IN_OCTAVE = len(NOTES)

errors = {
'notes': 'Bad input, please refer this spec-\n'
}


def swap_accidentals(note):
if note == 'Db':
return 'C#'
if note == 'D#':
return 'Eb'
if note == 'E#':
return 'F'
if note == 'Gb':
return 'F#'
if note == 'G#':
return 'Ab'
if note == 'A#':
return 'Bb'
if note == 'B#':
return 'C'

return note


def note_to_number(note: str, octave: int) -> int:
note = swap_accidentals(note)
assert note in NOTES, errors['notes']
assert octave in OCTAVES, errors['notes']

note = NOTES.index(note)
note += (NOTES_IN_OCTAVE * octave)

assert 0 <= note <= 127, errors['notes']

return note


array_of_notes = []
for chord in chord_progression:
array_of_notes.extend(chords.from_shorthand(chord))

array_of_note_numbers = []
for note in array_of_notes:
OCTAVE = 4
array_of_note_numbers.append(note_to_number(note, OCTAVE))

track = 0
channel = 0
time = 0 # In beats
duration = 1 # In beats
tempo = 120 # In BPM
volume = 100 # 0-127, as per the MIDI standard

MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track is created
# automatically)
MyMIDI.addTempo(track, time, tempo)

for i, pitch in enumerate(array_of_note_numbers):
MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)

with open("pure-edm-fire-arpeggio.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)

You’ll notice that there are few extra functions in that code. Unfortunately the tool `MIDIUtils` does not understand alphabetical characters like C but does understand their numerical equivalents i.e 60. Therefore, we need to make a little function to map the alphabetical characters to their corresponding numbers. The other weirdness in this code is a function called swap_accidentals . Like most systems, traditional music theory is not perfect and has oh, a few hundred years of quirks baked into it. IMO one of these quirks is the fact that you can have the exact same note referred to by different names. The note Bb is physically the same note as A# but in different contexts its referred to by a different names. Try not to worry about that too much right now because it’s just historic nuance that really doesn’t get us any closer towards our goal of making decent music with code. Just copy the function at face value for the time being.

After you have successfully run the code, you should end up with a midi-file called: `pure-edm-fire-arpeggio.mid`. On Mac OS you can just click on the file and it will play the notes with a default cheesy piano sound. Don’t worry, were not settling for cheese, we can load that file into a DAW such as Ableton and have powerful modern software synths generate the sound.

Before we load the arpeggio midi file into Ableton I want to modify that code to generate a bassline as well. Currently the code generates one note per beat at octave 4. We’re going to mod that code to generate just 1 note per bar (a bar meaning 4 beats) a few octaves lower. When both the midi files are played at the same time the will harmonize and fill out the audio spectrum with notes in a hi and low range. Go ahead and generate a bassline with this code:

from midiutil import MIDIFile
from mingus.core import chords

chord_progression = ["Cmaj7", "Cmaj7", "Fmaj7", "Gdom7"]

NOTES = ['C', 'C#', 'D', 'Eb', 'E', 'F', 'F#', 'G', 'Ab', 'A', 'Bb', 'B']
OCTAVES = list(range(11))
NOTES_IN_OCTAVE = len(NOTES)

errors = {
'notes': 'Bad input, please refer this spec-\n'
}


def swap_accidentals(note):
if note == 'Db':
return 'C#'
if note == 'D#':
return 'Eb'
if note == 'E#':
return 'F'
if note == 'Gb':
return 'F#'
if note == 'G#':
return 'Ab'
if note == 'A#':
return 'Bb'
if note == 'B#':
return 'C'

return note


def note_to_number(note: str, octave: int) -> int:
note = swap_accidentals(note)
assert note in NOTES, errors['notes']
assert octave in OCTAVES, errors['notes']

note = NOTES.index(note)
note += (NOTES_IN_OCTAVE * octave)

assert 0 <= note <= 127, errors['notes']

return note


array_of_notes = []
for chord in chord_progression:
array_of_notes.append(chords.from_shorthand(chord)[0])

array_of_note_numbers = []
for note in array_of_notes:
OCTAVE = 3
array_of_note_numbers.append(note_to_number(note, OCTAVE))

track = 0
channel = 0
time = 0 # In beats
duration = 1 # In beats
tempo = 120 # In BPM
volume = 100 # 0-127, as per the MIDI standard

MyMIDI = MIDIFile(1) # One track, defaults to format 1 (tempo track is created
# automatically)
MyMIDI.addTempo(track, time, tempo)

for i, pitch in enumerate(array_of_note_numbers):
MyMIDI.addNote(track, channel, pitch, time + (i*4), duration, volume)

with open("pure-edm-fire-bass.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)

Now that we have 2 midi files. An arpeggio line and a bassline. You can now drag and drop the files into any major DAW. I will demonstrate this in Ableton because of its popularity. Note: REAPER is an excellent choice if you’re not interested in paying for an Ableton licence. I created a quick video demonstrating this workflow. I happen to have chosen the Arturia Pigments soft synth to generate the sounds because I like it, but it’s an arbitrary choice for demonstration purposes.

So there you have it! You are now able to generate midi with code that is harmonically correct and can be used to create great sounding music!

NEXT STEPS:

We’ve really just scratched the surface of what’s possible. Some logical next steps might be to clean up that code and generate arpeggio pattern variations, more complex basslines and drum beats. I also recommend familiarizing yourself with some of the latests machine learning concepts. Many people including myself have achieved decent results generating melodies and harmonies with recurrent neural nets! Have fun!!

--

--