Spaces:
Runtime error
Runtime error
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" | |