Music sequencer
Download worked project
ABC is a popular format to write music notation in plain text files, you can see an example by openining tunes1.abc with a text editor. A music sequencer is an editor software which typically displays notes as a matrix: let’s see how to parse simplified abc tunes and display their melodies in such a matrix.
What to do
Unzip exercises zip in a folder, you should obtain something like this:
music-sequencer-prj
music-sequencer.ipynb
music-sequencer-sol.ipynb
tunes1.abc
jupman.py
WARNING: to correctly visualize the notebook, it MUST be in an unzipped folder !
open Jupyter Notebook from that folder. Two things should open, first a console and then a browser. The browser should show a file list: navigate the list and open the notebook
music-sequencer.ipynb
Go on reading the notebook, and write in the appropriate cells when asked
Shortcut keys:
to execute Python code inside a Jupyter cell, press
Control + Enter
to execute Python code inside a Jupyter cell AND select next cell, press
Shift + Enter
to execute Python code inside a Jupyter cell AND a create a new cell aftwerwards, press
Alt + Enter
If the notebooks look stuck, try to select
Kernel -> Restart
1. parse_melody
Write a function which given a melody as a string of notes translates it to a list of tuples:
>>> parse_melody("|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |")
[(0, 8), (2, 4), (4, 4), (2, 8), (4, 2), (3, 2), (2, 4), (2, 6), (1, 6), (6, 4)]
Each melody note is followed by its duration. If no duration number is specified, we assume it is one.
Each tuple first element represents a note as a number from 0
(A
) to 6
(G
) and the second element is the note length in the sequencer. We assume our sequencer has a resolution of two beats per note, so for us a note A
would have length 2
, a note A2
a length 4
, a note A3
a length 6
and so on.
DO NOT care about spaces nor bars
|
, they have no meaning at allDO NOT write a wall of
if
s, instead USE ord python function to get a character position
[1]:
def parse_melody(melody):
raise Exception('TODO IMPLEMENT ME !')
from pprint import pprint
melody1 = "|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |"
pprint(parse_melody(melody1) )
assert parse_melody("||") == []
assert parse_melody("|A|") == [(0,2)]
assert parse_melody("| B|") == [(1,2)]
assert parse_melody("|C |") == [(2,2)]
assert parse_melody("|A3|") == [(0,6)]
assert parse_melody("|A B|") == [(0,2), (1,2)]
assert parse_melody(" | G F | ") == [(6,2), (5,2)]
assert parse_melody("|D|B|") == [(3,2), (1,2)]
assert parse_melody("|D3 E4|") == [(3,6),(4,8)]
assert parse_melody("|F|A2 B|") == [(5,2),(0,4),(1,2)]
assert parse_melody("|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |") == \
[(0, 8), (2, 4), (4, 4), (2, 8), (4, 2), (3, 2), (2, 4), (2, 6), (1, 6), (6, 4)]
2. parse_tunes
An .abc
file is a series of key:value fields. Keys are always one character long. Anything after a %
is a comment and must be ignored
File tunes1.abc EXCERPT:
[2]:
with open("tunes1.abc", encoding='utf-8') as f: print(''.join(f.readlines()[0:18]))
%abc-2.1
H:Tune made in a dark algorithmic night % history and origin in header, so replicated in all tunes!
O:Trento
X:1 % index
T:Algorave % title
C:The Lord of the Loop % composer
M:4/4 % meter
K:C % key
|A4 C2 E2 |C4 E D C2 |C3 B3 G2 | % melodies can also have a comment
X:2
T:Transpose Your Head
C:Matrix Queen
O:Venice % overriding header
M:3/4
K:G
|F2 G4 |E4 E F|A2 B2 D2 |D3 E3 |C3 C3 |
First lines (3 in the example) are the file header, separated by tunes with a blank line.
first line must always be ignored
fields specified in the file header must be copied in all tunes
Note a tune may override a field (es
O:Venice
).
After the first blank line, there is the first tune:
X
is the tune index, convert it to integerM
is the meter, convert it to a tuple of two integersK
is the last field of metadatamelody line has no field key, it always follows line with
K
and it immediately begins with a pipe: convert it to list by callingparse_melody
Following tunes are separated by blank lines
Write a function parse_tunes
which parses the file and outputs a list of dictionaries, one per tune. Use provided field_names
to obtain dictionary keys. Full expected db is in expected_db1.py file.
DO NOT write hundreds of ifs
Special keys are listed above, all others should be treated in a generic way
DO NOT assume header always contains 'origin'
and 'history'
It can contain any field, which has to be then copied in all the tunes, see tunes2.abc for extra examples.
Example:
>>> tunes_db1 = parse_tunes('tunes1.abc')
>>> pprint(tunes_db1[:2],width=150)
[
{'composer': 'The Lord of the Loop',
'history': 'Tune made in a dark algorithmic night',
'index': 1,
'key': 'C',
'melody': [(0, 8), (2, 4), (4, 4), (2, 8), (4, 2), (3, 2), (2, 4), (2, 6), (1, 6), (6, 4)],
'meter': (4, 4),
'origin': 'Trento',
'title': 'Algorave'
},
{'composer': 'Matrix Queen',
'history': 'Tune made in a dark algorithmic night',
'index': 2,
'key': 'G',
'melody': [(5, 4),(6, 8),(4, 8),(4, 2),(5, 2),(0, 4),(1, 4),(3, 4),(3, 6),(4, 6),(2, 6),(2, 6)],
'meter': (3, 4),
'origin': 'Venice',
'title': 'Transpose Your Head'
}
]
[3]:
field_names = {
'C':'composer',
'D':'discography',
'H':'history',
'K':'key',
'M':'meter',
'O':'origin',
'T':'title',
'X':'index',
}
def parse_tunes(filename):
raise Exception('TODO IMPLEMENT ME !')
tunes_db1 = parse_tunes('tunes1.abc')
pprint(tunes_db1[:3],width=150)
[4]:
assert tunes_db1[0]['history']=='Tune made in a dark algorithmic night'
assert tunes_db1[0]['origin']=='Trento'
assert tunes_db1[0]['index']==1
assert tunes_db1[0]['title']=='Algorave'
assert tunes_db1[0]['composer']=='The Lord of the Loop'
assert tunes_db1[0]['meter']==(4,4)
assert tunes_db1[0]['key']== 'C'
assert tunes_db1[0]['melody']==\
[(0, 8), (2, 4), (4, 4), (2, 8), (4, 2), (3, 2), (2, 4), (2, 6), (1, 6), (6, 4)]
assert tunes_db1[1]['history']=='Tune made in a dark algorithmic night'
assert tunes_db1[1]['origin']=='Venice' # tests override
assert tunes_db1[1]['index']==2
assert tunes_db1[1]['title']=='Transpose Your Head'
assert tunes_db1[1]['composer']=='Matrix Queen'
assert tunes_db1[1]['meter']==(3,4)
assert tunes_db1[1]['key']== 'G'
assert tunes_db1[1]['melody']==\
[(5, 4), (6, 8), (4, 8), (4, 2), (5, 2), (0, 4), (1, 4), (3, 4), (3, 6), (4, 6), (2, 6), (2, 6)]
from expected_db1 import expected_db1
assert len(tunes_db1) == len(expected_db1)
assert tunes_db1 == expected_db1
tunes_db2 = parse_tunes('tunes2.abc')
pprint(tunes_db2)
from expected_db2 import expected_db2
assert tunes_db2 == expected_db2
3. sequencer
Write a function sequencer
which takes a melody in text format and outputs a matrix of note events, as a list of strings.
The rows are all the notes on keyboard (we assume 7 notes without black keys) and the columns represent the duration of a note.
a note start is marked with
<
character, a sustain with=
character and end with>
HINT 1: call
parse_melody
to obtain notes as a list of tuples (if you didn’t manage to implement it copy expected list from expected_db1.py)HINT 2: build first a list of list of characters, and only at the very end convert to a list of strings
HINT 3: try obtaining the note letters for first column by using
ord
andchr
Example 1:
>>> from pprint import pprint
>>> melody1 = "|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |"
>>> res1 = sequencer(melody1)
>>> print(' ' + melody1)
|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |
>>> pprint(res1)
['A<======> ',
'B <====> ',
'C <==> <======> <==><====> ',
'D <> ',
'E <==> <> ',
'F ',
'G <==>']
Example 2:
>>> melody2 = "|F2 G4 |E4 E F|A2 B2 D2 |D3 E3 |C3 C3 |"
>>> res2 = sequencer(melody2)
>> print(' ' + melody2)
|F2 G4 |E4 E F|A2 B2 D2 |D3 E3 |C3 C3 |
>>> pprint(res2)
['A <==> ',
'B <==> ',
'C <====><====>',
'D <==><====> ',
'E <======><> <====> ',
'F<==> <> ',
'G <======> ']
[5]:
def sequencer(melody):
raise Exception('TODO IMPLEMENT ME !')
from pprint import pprint
melody1 = "|A4 C2 E2 |C4 E D C2 |C3 B3 G2 |"
exp1 = [
'A<======> ',
'B <====> ',
'C <==> <======> <==><====> ',
'D <> ',
'E <==> <> ',
'F ',
'G <==>']
res1 = sequencer(melody1)
print(' ' + melody1)
print()
pprint(res1)
assert res1 == exp1
[6]:
from pprint import pprint
melody2 = "|F2 G4 |E4 E F|A2 B2 D2 |D3 E3 |C3 C3 |"
exp2 = ['A <==> ',
'B <==> ',
'C <====><====>',
'D <==><====> ',
'E <======><> <====> ',
'F<==> <> ',
'G <======> ']
res2 = sequencer(melody2)
print(' ' + melody2)
print()
pprint(res2)
assert res2 == exp2
4. plot_tune
Make it fancy: write a function which takes a tune dictionary from the db and outputs a plot
use beats as xs, remembering the shortest note has two beats
to increase thickness, use
linewidth=5
parameter
[7]:
%matplotlib inline
import matplotlib.pyplot as plt
def plot_tune(tune):
raise Exception('TODO IMPLEMENT ME !')
plot_tune(tunes_db1[0])
[ ]: