Generating MIDI files from guitar tablatures with Python and regular expressions
Last updated April 26, 2018
Recently I have been playing lots of classical guitar. I use classtab.org, a website with hundreds of classical guitar pieces in tab form. Here's an example of a guitar tab:
|---------------0---|---3-2-3-----------|-----0-------------|
|-------0-3-1-1---3-|-3-------3-0-----0-|-1-3---3-1-0---0---|
|---0-2-0-----------|-------------0-2---|-------------2---0-|
|-------------2-----|-------2-----------|-------------------|
|-------------------|-2-----------------|-0-----2-----3-----|
|-3-----------------|-------------0-----|-------------------|
It is a representation of where and when strings are played. The six lines here represent the six strings of the guitar e, A, D, G, B and E and the numbers indicate where on the fretboard to press for each note. The vertical bars represent one bar of music.
My goal is to write an application that can read in guitar tabs and then produce a corresponding MIDI file for the song.
To start, I will use regular expressions to parse the bars of music in a guitar tab text file.
Next, I will use a Python package called MIDIUtil to generate .mid
files from the guitar tabs.
Finally, I'll put this into a simple web app that lets users copy paste guitar tab text or enter a URL from a site like classtab.org that will similarly generate a MIDI file.
Regex
First, let's look at a few different examples of guitar tabs to get a better sense of what our goal should be in using regular expressions to parse notes.
Tab examples
http://www.classtab.org/barrios_la_catedral_3_allegro.txt
La Catedral
Author: Agustin Barrios
Tablature by: Gianpiero Ciammaricone
giancian@yahoo.com
http://www.geocities.com/vienna/6619
Tablature Explanation at the end.
[key corrected from "D Major" Jan 99]
This is Revision 3 (5/2/98), I have added a modification made by
Barrios in line 5 of the TAB, both ways of playing it are included,
so you can choose the best one for you.
Revision 2: Made a few corrections.
Revision 1: The first release of the tab.
3rd Movement (Allegro):
Key: B Minor
Time signature: 6/8
E-|-------------------------|-----------------------------|
B-|-----------------3-------|-----------------3-----------|
G-|---------0---4-------4---|---------0---4-------4-------|
D-|---4-3h4---4---4---4---4-|---4-3h4---4---4---4---4-----|
A-|-2-----------------------|-2---------------------------|
E-|-------------------------|-----------------------------|
p m i m i m i a i m i
E-|-------------------------|-----------------------------|
B-|-----------------3-------|-----------------3-----------|
G-|---------2---4-------4---|---------2---4-------4-------|
D-|---5-4h5---5---5---5---5-|---5-4h5---5---5---5---5-----|
A-|-2-----------------------|-2---------------------------|
E-|-------------------------|-----------------------------|
p m i m i m i a i m i
E-|-------------------------|-----------------------------|
B-|-----------------5-------|-----------------5-----------|
G-|---------4---6-------6---|---------4---6-------6-------|
D-|---7-6h7---7---7---7---7-|---7-6h7---7---7---7---7-----|
A-|-4-----------------------|-4---------------------------|
E-|-------------------------|-----------------------------|
p m i m i m i a i m i
http://www.classtab.org/moreno_torroba_siete_piezas_de_album_6_chisperada.txt
SIETE PIEZAS DE ALBUM: VI. CHISPERADA
As recorded by Federico Moreno Torroba
Music by Federico Moreno Torroba
|-----0-----0-|-----------0-|-----0-----0-|
|---1-----0---|-----3-----0-|---1-----0---|
|---2-----0---|---2-------1-|---2-----0---|
|---2---------|---3---------|---2---------|
|-0-----------|---------2---|-0-----------|
|-------3-----|-1-----0-----|-------3-----|
|-----------0-|-----0-----0-|-----------0-|-----------0--|
|-----3-----0-|---1-----0---|-----3-----0-|-----3-----0--|
|---2-------1-|---2-----0---|---2-------1-|---2-------1--|
|---3---------|---2---------|---3---------|---3----------|
|---------2---|-0-----------|-0-------2---|-0-------2----|
|-1-----0-----|-------3-----|-------0-----|-------0------|
|---------------0--|-0-----0-----|-1-----1-3p1-0---|
|---1-0---------0--|-1-----------|---------------3-|
|-2-----2-0-----1--|-2-----1-----|-2-----2---------|
|-----------3------|-------------|-------0---------|
|-3----------------|---0-0---2-2-|---3-3-----------|
|-------------0----|-------------|-----------------|
http://www.classtab.org/moreno_torroba_sonatina_in_a_2_andante.txt
Sonatina in A - II - Andante
F. Moreno Torroba (1891-1982)
tuning DADGBE
E|-----2--3---5----------10--8--7-------|-----8----7^8^7-----------------------|
B|-----3--5---7----------7--------------|-----8-----------10---------8---------|
G|-----2---------------------9--7-------|-----9--------------------------11----|
D|-----0--------------4-----------------|--------------------------10----------|
A|-----0----------0---------------------|----------------------7---------------|
D|-----0--------------------------------|-----0--------------------------------|
E|--------------------------------------|--------------------------------------|
B|------------------7--8^10-8---7^5-----|---5---------7-8-7-5-----5------------|
G|--9^11^9----7--9------------------7---|---6-----------------7---6------------|
D|-------8------------------------------|---5---------------------5--7----19°--|
A|-------10-----------------------------|------0--12°--------------------------|
D|-------0------------------------------|---0---------------------0------------|
Here are a few things that we need to consider:
- The start of each line sometimes starts with a the note of the string (
E
ore
,A
,D
, etc.) followed by|
or just simply start with|
. Some lines may start withE-|
. - There is generally a
-
character between each note. When this is not the case, we may have ap
,h
or^
which indicates "pull off" or "hammer on". This means that you are playing the string with the hand on the fretboard, not the hand over the sound hole. We should replace these with-
. - A tab may have an inconsistant number of characters in each bar. I'm not too concerned about this. I'm not going for perfection just yet, but it could make this task very difficult if you want to get the timing of your sounds just right.
- Notes played on the 10th fret and higher must be read together. For example,
---12---
should be played on the 12 fret; it does not mean play1
and then play2
. - There are some other special characters such as
°
or<
/>
. I think these both denote harmonics. This is when you place your finger on the string at a position along the fretboard, but do not press down. We probably want to replace these characters with-
as well.
Tab text file processing
Here are some things that may be helpful to do as soon as we read a text file:
- convert everything to upper or lower case
- replace non-numeric characters with
-
When I work with regex, I usually go straight to regex101.com.
MIDIUtil
At this point, I think it will be helpful to examine MIDIUtil and create and play a basic MIDI file.
Before you do that, you might want to read over the MIDI Arch Wiki article.
MIDIUtil is a pure Python library that allows one to write multi-track Musical Instrument Digital Interface (MIDI) files from within Python programs (both format 1 and format 2 files are now supported). It is object-oriented and allows one to create and write these files with a minimum of fuss.
Here's a basic example of MIDIUtil. This script generates a MIDI file that contains a major scale.
#!/usr/bin/env python
from midiutil import MIDIFile
degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
track = 0
channel = 0
time = 0 # In beats
duration = 1 # In beats
tempo = 60 # 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(degrees):
MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
with open("major-scale.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)
Now let's try to play this file. Well, if you read the MIDI article, you would know that this isn't really what we are doing. We want to see what our major scale sounds like. In order to do that, we need to install and configure both a MIDI synthesizer as well as a soundfont.
We can use timidity++ for our MIDI synth. There are very simple instructions on how to do this here.
Here are the steps I followed:
From the AUR, install timidity++
and timidity-freepats
.
Add yourself to the audio group:
# gpasswd -a brian audio
Add the following line to /etc/timidity++/timidity.cfg
:
soundfont /usr/share/soundfonts/timidity-freepats.sf2
sudo systemctl start timidity.service
sudo systemctl enable timidity.service
Run aplaymidi -l
:
$ aplaymidi -l
Port Client name Port name
14:0 Midi Through Midi Through Port-0
130:0 TiMidity TiMidity port 0
130:1 TiMidity TiMidity port 1
130:2 TiMidity TiMidity port 2
130:3 TiMidity TiMidity port 3
Finally, we can play our MIDI file that we generated by running the python script above:
$ aplaymidi major-scale.mid --port 130:0
<music plays...>
For this project, playing files using a synthesizer is useful to verify that the MIDI was successfully created. If/when we get to the point of making a web app to convert midi files to music files, we will generate the midifile in-memory, convert it to a WAV file and then return that to the user.
Let's try to process a very simple tab file with just one string and a few notes, and then generate a simple MIDI file based on the tab.
Our sample tab can be:
simple-tab.txt
--0--2--4--
#!/usr/bin/env python
from midiutil import MIDIFile
degrees = [60, 62, 64, 65, 67, 69, 71, 72] # MIDI note number
track = 0
channel = 0
time = 0 # In beats
duration = 1 # In beats
tempo = 60 # 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(degrees):
MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
with open("major-scale.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)
$ echo "--0--2--4--" > simple-tab.txt
Here's the script I used to generate some fairly accurate MIDI files. There are still some bugs in the script that generates the MIDI file, but this meets the goal I had last weekend of writing a simple script to generate music from guitar tabs.
#!/usr/bin/env python
from midiutil import MIDIFile
import sys
import re
string_notes = ["high_e", "B", "G", "D", "A", "E"]
guitar_strings = {
'E':{'note_val':52, 'track_num':0},
'A':{'note_val':57, 'track_num':1},
'D':{'note_val':62, 'track_num':2},
'G':{'note_val':67, 'track_num':3},
'B':{'note_val':71, 'track_num':4},
'high_e':{'note_val':76, 'track_num':5},
}
# read the tab file
file_name = sys.argv[1]
if file_name.split(".")[-1] != 'txt':
print("Please select a text file")
with open(file_name) as f:
contents = f.read()
contents = contents.replace("h", "-")
contents = contents.replace("p", "-")
contents = contents.replace("/", "-")
contents = contents.replace("*", "-")
contents = contents.upper()
bar_group = re.findall(r"(?:[E,B,G,D,A,-]+\|[0-9-h|]+\n){6}",contents)
#bar_group = re.findall(r"(?:\|[0-9-\*h\|]+\n){6}",contents)
track = 0
channel = 0
time = 0 # In beats
duration = 1 # In beats
tempo = 1000 # In BPM
volume = 100 # 0-127, as per the MIDI standard
MyMIDI = MIDIFile(6) # One track, defaults to format 1 (tempo track is created
# automatically)
MyMIDI.addTempo(track, time, tempo)
interval = len(bar_group[0].split("\n")) - 1
for b in bar_group:
strings = b.split("\n")
strings = [x for x in strings if x != '']
e_count = 0
for i,s in enumerate(strings):
current_string = strings[i][0]
if current_string not in guitar_strings.keys():
current_string = string_notes[i]
if current_string == "E":
e_count += 1
if e_count == 2:
current_string == "high_e"
track = guitar_strings[current_string]['track_num']
s = s[1:]
s = s.replace('|', '')
s = list(s)
for i, pitch in enumerate(s):
volume = 100
if pitch == "\n":
break
if pitch == "-":
volume = 0
pitch = 50
print("adding note")
pitch = int(pitch) + guitar_strings[current_string]['note_val']
MyMIDI.addNote(track, channel, pitch, time + i, duration, volume)
time += interval*8
with open("major-scale.mid", "wb") as output_file:
MyMIDI.writeFile(output_file)
Here's me playing one of my favorite songs, you might recognize it!