Music sequencer

Download worked project

Browse files online

expected-plot-preview

ABC is a popular format to write music notation in plain text files, you can see an example by openining tunes.abc with a text editor. A music sequencer is an editor software which typically displays notes as a matrix: you will parse simplified abc tunes and display their melodies in a matrix.

What to do

  1. Unzip exercises zip in a folder, you should obtain something like this:

music-sequencer-prj
    music-sequencer.ipynb
    music-sequencer-sol.ipynb
    tunes.abc
    jupman.py

WARNING: to correctly visualize the notebook, it MUST be in an unzipped folder !

  1. 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

  2. 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 all

  • USE ord python function to get a character position

Show solution
[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("|A3|") == [(0,6)]
assert parse_melody("|A B|") == [(0,2), (1,2)]
assert parse_melody("|C D|") == [(2,2), (3,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 tunes.abc excerpt:

[2]:
with open("tunes.abc", encoding='utf-8') as f: print(''.join(f.readlines()[0:17]))
%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
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

After the first blank line, there is the first tune:

  • X is the tune index, convert it to integer

  • M is the meter, convert it to a tuple of two integers

  • K is the last field of metadata

  • melody line has no field key, it always follows line with K and it immediately begins with a pipe: convert it to list by calling parse_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_db.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.

Example:

>>> tunes_db = parse_tunes('tunes.abc')
>>> pprint(tunes_db[: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': 'Trento',
  'title': 'Transpose Your Head'
 }
]
Show solution
[3]:


field_names = {
    'O':'origin',
    'H':'history',
    'X':'index',
    'T':'title',
    'C':'composer',
    'M':'meter',
    'K':'key',
}

def parse_tunes(filename):
    raise Exception('TODO IMPLEMENT ME !')

tunes_db = parse_tunes('tunes.abc')
pprint(tunes_db[:2],width=150)

[4]:
assert tunes_db[0]['history']=='Tune made in a dark algorithmic night'
assert tunes_db[0]['origin']=='Trento'
assert tunes_db[0]['index']==1
assert tunes_db[0]['title']=='Algorave'
assert tunes_db[0]['composer']=='The Lord of the Loop'
assert tunes_db[0]['meter']==(4,4)
assert tunes_db[0]['key']== 'C'
assert tunes_db[0]['melody']==\
[(0, 8), (2, 4), (4, 4), (2, 8), (4, 2), (3, 2), (2, 4), (2, 6), (1, 6), (6, 4)]
assert tunes_db[1]['history']=='Tune made in a dark algorithmic night'
assert tunes_db[1]['origin']=='Trento'
assert tunes_db[1]['index']==2
assert tunes_db[1]['title']=='Transpose Your Head'
assert tunes_db[1]['composer']=='Matrix Queen'
assert tunes_db[1]['meter']==(3,4)
assert tunes_db[1]['key']== 'G'
assert tunes_db[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_db import expected_db
assert tunes_db == expected_db

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_db.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 and chr

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    <======>                                                ']
Show solution
[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

expected-plot.png

Show solution
[7]:

%matplotlib inline
import matplotlib.pyplot as plt

def plot_tune(tune):
    raise Exception('TODO IMPLEMENT ME !')

plot_tune(tunes_db[0])
[ ]: