File size: 9,729 Bytes
7f54642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61aa7f2
 
7f54642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61aa7f2
7f54642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247

import os,glob
from matplotlib.pyplot import pie
from mido import MidiFile
import datetime
import numpy as np
import pandas as pd
import subprocess
from music21 import *
from music21 import converter
from mido import Message, MidiFile, MidiTrack, MetaMessage

#number of notes to be used for prediction
window = 3

#num of notes to generate
#TODO: change this to accept values according to user
num_notes = 100

#midi ticks per quarter note, indicates tempo of track
quarter_note_ticks = 480

#accepted note durations: ranges from 16th note to whole dotted notes
accepeted_lengths = [0.25,0.375,0.5,0.75,1,1.5,2.0,3.0,4.0]
#Finds all absolute paths in directory
#https://stackoverflow.com/questions/9816816/get-absolute-paths-of-all-files-in-a-directory
def abs_paths(dir):
    for dir_path,_,filenames in os.walk(dir):
        for f in filenames:
            yield os.path.abspath(os.path.join(dir_path, f))
def pitch_to_int(nameWithOctave):
    # letter names with corresponding values
    letter_dict = {'C':0,'D':2,'E':4,'F':5,'G':7,'A':9,'B':11}
    # parse characters from strings
    chars = list(nameWithOctave)
    # convert octave number to corresponding midi value
    octave = 12*(int(chars[-1])+1)
    # select value from letter_dict using first character
    note = letter_dict[chars[0]]
    # set accidental value
    accidental = 0
    # does accidental exist?
    if not len(chars)==2:
        # increase (sharp) or decrease (flat) value by one
        accidental = 1 if chars[1]=='#' else -1
    # return sum of these numbers, middle C(4) == 60
    return octave + note + accidental
def get_pngs(path):
	filelist=os.listdir(path)
	for fichier in filelist[:]: # filelist[:] makes a copy of filelist.
		if not(fichier.endswith(".png")):
			filelist.remove(fichier)
	newlist = [path+'/'+x for x in filelist] #making it cwd
	return newlist
def generate_notes(csv_file):
    df_notes = pd.read_csv(csv_file)
    print(df_notes.shape)
    # define arrays for generated notes and durations
    gen_notes = []
    gen_durations = []
    # define note and duration feature columns based on names
    features = df_notes.columns[:-2]
    note_features = [s for s in features if "note" in s]
    duration_features = [s for s in features if "duration" in s]
    # define target columns
    note_target = df_notes.columns[-2]
    duration_target = df_notes.columns[-1]

    # sample random row from dataframe and define start notes and durations
    initial_sample = df_notes.sample()
    start_notes = list(initial_sample[note_features].values[0])
    start_durations = list(initial_sample[duration_features].values[0])
    # append starting notes and durations to gen arrays
    for note in start_notes:
        gen_notes.append(int(note))
    for duration in start_durations:
        gen_durations.append(duration)

    for i in range(num_notes) :
        rows = df_notes
        for i in range(window-1):
            rows = rows.loc[df_notes[note_features[i]] == start_notes[i]]
            rows = rows.loc[df_notes[duration_features[i]]== start_durations[i]]
    
        #This gives the same effect as probability.
        # We effectively sample from a list which might have more than 1 C note, Hence increasing its probability
        #Sometime, The start_notes and durations could be selected in such a way that we cannot generate any further notes uptill num_notes,
        #This means there maybe some combinations of notes such as 76,68 which are not there in the dataset and hence cannot be sampled.
        #In such cases, the only way about it would be to reset the start notes, because we cannot sample from an empty row 
        #So here we check if any rows which we ta
        if len(rows):
            next_sample = rows.sample()
            next_note = next_sample[note_target].values[0]
            next_duration = next_sample[duration_target].values[0]
            gen_notes.append(int(next_note))
            gen_durations.append(next_duration)

            start_notes.pop(0)
            start_durations.pop(0)

            start_notes.append(next_note)
            start_durations.append(next_duration)
        else:
            #Received empty row
            # print("Exiting!!!!!!")
            #restarting again to get new start notes
            return [],[]
           
        # print(rows[note_target].value_counts(normalize=True))
        # print(rows[duration_target].value_counts(normalize=True))

    return gen_notes, gen_durations

#MAIN FUNCTION
def main_markov(time_sign):
    command = "rm -r MC/gen_songs_midi/*"
    subprocess.Popen(command,shell=True,stdout=subprocess.PIPE,stderr=subprocess.PIPE).communicate()
    # https://stackoverflow.com/questions/49462107/how-can-i-get-all-piano-parts-from-a-music21-score
    if not os.path.exists('tracks'):
        os.mkdir('tracks')
        os.mkdir('tracks/3_4')
        os.mkdir('tracks/4_4')
        os.mkdir('tracks/6_8')
        os.mkdir('tracks/2_2')
        os.mkdir('tracks/2_4')
        i = 0
        #Parse midi files into tracks folder

        
        for path in abs_paths('data'):
            print(path)
            piece = converter.parse(path)
            #print(list(piece.parts))
            # for part in piece.parts:
            part_notes = []
            l = piece.getTimeSignatures()
            
            # s = l.show('text') #prints piece time signature
            
            time_sig_num = piece.recurse().getElementsByClass(meter.TimeSignature)[0].numerator
            time_sig_denum = piece.recurse().getElementsByClass(meter.TimeSignature)[0].denominator #gets time signature for piece
            #print(piece[meter.TimeSignature][0])
            #print(piece['Measure'][0].timeSignature)
            
            for el in piece.recurse().notes:
                # print(el.offset, el, el.activeSite)
                # print(el.beatDuration)
                # print(el.quarterLength)
                # print(el._getTimeSignatureForBeat)
                # print(el.beat)
                
                # if getattr(el, 'isNote', None):
                #     print("this method works")
                #     print(el.nameWithOctave)
                #get all note messages from all tracks
                # for event in el:
                    # for y in event.contextSites():
                    #     if y[0] is part:
                    #         offset = y[1]
                
                if getattr(el, 'isNote', None) and el.isNote:
                    # print('note in {}'.format(el))
                    #check if note is in accepted length
                    #convert string to numerical value 
                    if el.quarterLength in accepeted_lengths:
                        part_notes.append([pitch_to_int(el.nameWithOctave), el.quarterLength])
            if not len(part_notes) == 0:
                np.save('tracks/'+str(time_sig_num)+'_'+str(time_sig_denum)+'/'+'{}.npy'.format(i), np.array(part_notes))
                i+=1
        print('Number of tracks parsed: {}'.format(i))
    if not glob.glob('MC/prepared*.csv'):
        sigs = ['3_4','4_4','6_8','2_2','2_4']
        columns = []
        for i in range(window):
            columns.append('note' + str(i))
            columns.append('duration' + str(i))
        for sig in sigs:
            df_notes = pd.DataFrame(columns=columns)
            # append segments from each track as rows to dataframe
            for path in abs_paths('tracks/'+sig):
                notes = np.load(path)
                for i in range(len(notes)-window):
                    # take every x notes and durations
                    segment = notes[i:i+window].flatten()
                    # make into pd.Series row
                    row = pd.Series(segment, index=df_notes.columns)
                    # append row to dataframe
                    df_notes = df_notes.append(row, ignore_index=True)
            # export
            df_notes.to_csv('prepared'+sig+'.csv', index=False)
    time_signature = str(time_sign).split('/')

    success = False
    gen_notes =[]
    gen_durations =[]

    #Retry mechanism
    csv_path = 'MC/prepared'+time_signature[0]+'_'+time_signature[1]+'.csv'
    while len(gen_notes)<num_notes:
        gen_notes,gen_durations = generate_notes(csv_path)


    print('Generated  notes/durations'.format(num_notes))
    print(gen_notes)
    print(gen_durations)

    #for unique file name
    dob = datetime.datetime.now().strftime('%H%M%S')
    modifier = format(dob)

    path = "MC/gen_songs_midi/song_"+modifier
    song_path = path + "/gen_song_"+modifier
    if not os.path.exists(path):
        os.makedirs(path)

    mid = MidiFile()
    track = MidiTrack()
       
    mid.tracks.append(track)
    print(mid)
    track.append((MetaMessage('time_signature', numerator=int(time_signature[0]), denominator=int(time_signature[1]))))
    # https://mido.readthedocs.io/en/latest/midi_files.html
    for i in range(num_notes):
        track.append(Message('note_on', channel=0, note=gen_notes[i], velocity=60, time=0))
        track.append(Message('note_on', channel=0, note=gen_notes[i], velocity=0,time=int(gen_durations[i]*quarter_note_ticks)))
    mid.save(song_path+".mid")

    subprocess.Popen(['midi2ly','-o',song_path+'.ly',song_path+'.mid']).communicate()
    subprocess.Popen(['lilypond','-fpng','-o',path,song_path+".ly"]).communicate()
    subprocess.Popen(['timidity',song_path+'.midi','-Ow','-o',song_path+'.wav']).communicate()
    png_list = get_pngs(path)

    return png_list,song_path+".wav"




# LILYPOND COMMANDS : To be used for generating music scores
# Installation : sudo apt-get install -y lilypond
# !midi2ly "new_song.ly"
# !lilypond -fpng "new_song-midi.ly"