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

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

  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

  • DO NOT write a wall of ifs, instead 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("| 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 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_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'
 }
]
Show solution
[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 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_db1[0])
[ ]: