def __init__(self, **kwargs): """ Initial class. Here i used **kwargs because I want to handle named arguments """ super(SongsToLearnApp, self).__init__(**kwargs) self.song_collection = SongCollection() self.show_song = []
def __init__(self, directories, *args, **kwargs): print(args, kwargs) tk.Tk.__init__(self, *args, **kwargs) # Stream for audio playback self.pa = pyaudio.PyAudio() self.stream = self.pa.open(format = pyaudio.paFloat32, channels=1, rate=44100, output=True) self.protocol("WM_DELETE_WINDOW", self.close_window) sc = SongCollection() print(directories) for dir_ in directories: sc.load_directory(dir_) self.songs = sc.get_marked() self.song = None self.varSongSelector = tk.Variable(self) self.songSelector = tk.OptionMenu(self, self.varSongSelector, '', *tuple([s.title for s in self.songs]), command = self.select_song) self.songSelector.grid(row=0,columnspan=3) tk.Label(self, text="Beats: ").grid(row=1) self.b_beats_play = tk.Button(self, text="[ > ]", command=self.play_beats) self.b_beats_play.grid(row=1,column=1) self.b_beats_fix = tk.Button(self, text="[+1]", command=self.shift_beats) self.b_beats_fix.grid(row=1,column=2) tk.Label(self, text="Downbeats: ").grid(row=2) self.b_downbeats_play = tk.Button(self, text="[ > ]", command=self.play_downbeats) self.b_downbeats_play.grid(row=2,column=1) self.b_downbeats_fix = tk.Button(self, text="[+1]", command=self.shift_downbeats) self.b_downbeats_fix.grid(row=2,column=2) tk.Label(self, text="Segments: ").grid(row=3) self.b_segments_shift_min8 = tk.Button(self, text="[-8]", command=lambda : self.shift_segments(-8)) self.b_segments_shift_min8.grid(row=3,column=1) self.b_segments_shift_min1 = tk.Button(self, text="[-1]", command=lambda : self.shift_segments(-1)) self.b_segments_shift_min1.grid(row=3,column=2) self.b_segments_shift_plus1 = tk.Button(self, text="[+1]", command=lambda : self.shift_segments(1)) self.b_segments_shift_plus1.grid(row=3,column=3) self.b_segments_shift_plus8 = tk.Button(self, text="[+8]", command=lambda : self.shift_segments(8)) self.b_segments_shift_plus8.grid(row=3,column=4) self.segment_buttons = [] # TODO How to insert segments? self.b_save = tk.Button(self, text="[ SAVE CHANGES ]", command=self.save) self.b_save.grid(row=0,column=4)
def sort_key(collection, key): """Ask for the order as descending or ascending""" descending = handle_text('Sort songs in descending order? (Y/N): ', is_bool=True) print("Results:") sort_results = collection.sort_song(key, descending) print(SongCollection.format_data(sort_results))
def build(self): SongCollection() # initialise an empty list on program start Window.size = (900, 600) self.title = 'Songs to Learn 2.0' self.root = Builder.load_file('app.kv') songs_list = self.load_songs() self.create_entry_grids(songs_list) self.root.ids.message_box.text = 'Welcome to Songs to Learn 2.0' return self.root
def search_songs(collection): """Search the song by keywords that the users input""" flag = False while not flag: print("Search songs") title_search = handle_text('Title (keywords):', blank=True) year_search = handle_text('Year:', blank=True) artist_search = handle_text("Artist", blank=True) status_search = handle_text("Learned or unlearned? (Y/N)?", is_bool=True, blank=True) print("Here are the results: ") collection.search_song(title_search, year_search, artist_search, status_search) print(SongCollection.format_data(collection.search_list)) choice = handle_text("Continue? (Y/N): ", is_bool=True) if not choice: collection.search_list = [] flag = True
def run_tests(): """Test SongCollection class.""" # Test empty SongCollection (defaults) print("Test empty SongCollection:") song_collection = SongCollection() print(song_collection) assert not song_collection.songs # an empty list is considered False # Test loading songs print("Test loading songs:") song_collection.load_songs('songs.csv') print(song_collection) assert song_collection.songs # assuming CSV file is non-empty, non-empty list is considered True # Test adding a new Song with values print("Test adding new song:") song_collection.add_song(Song("My Happiness", "Powderfinger", 1996, True)) print(song_collection) # Test sorting songs print("Test sorting - year:") song_collection.sort("year") print(song_collection)
import sys from essentia import * from essentia.standard import * from sklearn.decomposition import PCA from sklearn import preprocessing import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np if __name__ == '__main__': scaler = preprocessing.StandardScaler() pca = PCA(n_components=3) sc = SongCollection() for dir_ in sys.argv[1:]: sc.load_directory(dir_) pool = Pool() songs = [] for song in sc.get_annotated(): song.open() pool.add('themefeatures', song.spectral_contrast) songs.append(song.title) song.close() #~ # Test 1: spectral centroid on itself #~ X = pool['spectral_centroid'] #~ plt.figure()
def run_tests(): """Test SongCollection class.""" # Test empty SongCollection (defaults) print("Test empty SongCollection:") song_collection = SongCollection() print(song_collection) assert not song_collection.song_list # an empty list is considered False # Test loading songs print("Test loading songs:") song_collection.load_songs('songs.csv') print(song_collection) assert song_collection.song_list # assuming CSV file is non-empty, non-empty list is considered True # Test adding a new Song with values print("Test adding new song:") song_collection.add_song(Song("My Happiness", "Powderfinger", 1996, True)) print(song_collection) # Test sorting songs print("Test sorting - year:") sorted_list = song_collection.sort_song("year", descending=True) print(song_collection.format_data(sorted_list)) # Test sorting artists print("Test sorting - artist:") sorted_list_2 = song_collection.sort_song("artist", descending=True) print(song_collection.format_data(sorted_list_2)) # test sorting titles print("Test sorting - title:") sorted_list_3 = song_collection.sort_song("title", descending=False) print(song_collection.format_data(sorted_list_3)) # Test saving songs (check CSV file manually to see results) print("Test saving songs:") song_collection.save_changes("songs.csv") print("Open songs.csv to check whether new song appears or not") # Add more tests, as appropriate, for each method # Test count learned songs print("Test count learned songs:") print(song_collection.count_learned()) # Test count unlearned songs print("Test count unlearned songs:") print(song_collection.count_unlearned())
#~ LOG_LEVEL = logging.INFO LOG_LEVEL = logging.DEBUG LOGFORMAT = "%(log_color)s%(message)s%(reset)s" from colorlog import ColoredFormatter logging.root.setLevel(LOG_LEVEL) formatter = ColoredFormatter(LOGFORMAT) stream = logging.StreamHandler() stream.setLevel(LOG_LEVEL) stream.setFormatter(formatter) logger = logging.getLogger('colorlogger') logger.setLevel(LOG_LEVEL) logger.addHandler(stream) if __name__ == '__main__': sc = SongCollection() tl = TrackLister(sc) dj = DjController(tl) essentia.log.infoActive = False essentia.log.warningActive = False while (True): try: cmd_split = str.split(raw_input('> : '), ' ') except KeyboardInterrupt: logger.info('Goodbye!') break cmd = cmd_split[0] if cmd == 'loaddir': if len(cmd_split) == 1:
class SongsToLearnApp(App): """ The main class for the GUI of the song app""" sort = StringProperty() category = ListProperty() order = ListProperty() current_order = StringProperty() def __init__(self, **kwargs): """ Initial class. Here i used **kwargs because I want to handle named arguments """ super(SongsToLearnApp, self).__init__(**kwargs) self.song_collection = SongCollection() self.show_song = [] def build(self): """Build app with the help of app.kv file""" # application window size Window.size = (1000, 800) self.title = "Songs to learn app" # load app.kv self.root = Builder.load_file('app.kv') self.category = ['Title', 'Year', 'Artist', 'Unlearn'] self.sort = self.category[0] self.order = ['Ascending Order', 'Descending Order'] self.current_order = self.order[0] return self.root def on_start(self): """This function start initially when the program start""" # Load songs from the csv file self.song_collection.load_songs('songs.csv') self.show_song = self.song_collection.song_list self.root.ids.song_list.bind( minimum_height=self.root.ids.song_list.setter('height')) # App's welcome message self.root.ids.message.text = "Click on songs to mark as learned" # Load songs to the app self.load_songs() # Show the initial number of songs learned and songs unlearned self.count_learn() def on_stop(self): """ Save changes to the csv file after closing the program""" self.song_collection.save_changes('songs.csv') def count_learn(self): """Count songs learned and songs unlearned and display them in the GUI""" self.root.ids.learn_count.text = '{} songs learned, {} still to learn'.format( self.song_collection.count_learned(), self.song_collection.count_unlearned()) def handle_order(self, element): """Sort songs based on given order""" self.current_order = element self.load_songs() def sort_song(self, key): """Sort songs based on keywords""" self.sort = key self.load_songs() def handle_learn_song(self, instance): """Change songs status (Learned or unlearned) when users click on them""" current_song = instance.song current_song.is_learned = not current_song.is_learned self.load_songs() learn_mark = 'Learned' if current_song.is_learned else 'unlearned' self.root.ids.message.text = 'You have {} {}'.format( learn_mark, current_song) self.count_learn() def load_songs(self): """Load songs to the GUI """ self.root.ids.song_list.clear_widgets() desc = self.current_order == 'Descending Order' self.show_song = self.song_collection.sort_song(self.sort, desc) for index, song in enumerate(self.show_song): learn_mark = 'Learned' if song.is_learned else '' btn = Button(text='{} - {} ({}) {}'.format(song.title, song.artist, song.year, learn_mark), size_hint_y=None, height=30) btn.song = song btn.bind(on_press=self.handle_learn_song) if learn_mark: btn.background_color = (1, 0.5, 0.5, 1) self.root.ids.song_list.add_widget(btn) def handle_add_song(self, title, year, artist): """Check valid input and display messages telling that they have added song to songs lost successfully""" if title and year and artist: title_check = self.handle_input(title, is_title=True) artist_check = self.handle_input(artist, is_artist=True) year_check = self.handle_input(year, is_year=True) if year_check and artist_check and title_check: clean_title = ' '.join(title.split()) pretty_title = capwords(clean_title) self.song_collection.add_song( Song(title_check, artist_check, year_check)) self.load_songs() self.show_popup_message( '{} have been add to song list'.format(pretty_title)) self.handle_clear_button(is_add=True) else: # Error popup when users fail to input all the information self.show_popup_message("All fields are required") def handle_input(self, input_data, is_title=False, is_year=False, is_artist=False, is_learn=False, blank=False): """Check whether the users input the valid data""" # Check year valid input: must be a number that is greater than 0 if blank and not input_data: return True else: if is_year: try: year = int(input_data) if year < 0: raise ValueError() return input_data.strip() except ValueError: self.show_popup_message( "Your year must be a number and greater than 0") # Check for valid artist input elif is_artist: # Check for appropriate artist name input if input_data.lower().strip() in [ '`', '~', '!', '@', '#', '$', '%', '^', '&', '*', '(', ')', '_', '-', '+', '=', '{', '[', '}', '}', '|' ]: self.show_popup_message( "Please enter an appropriate artist name") else: return input_data.strip() # Check learn/unlearned status elif is_learn: if input_data.lower() not in ["y", 'n']: self.show_popup_message('Learn field must be Y or N') else: return True elif not input_data.strip() and is_title: self.show_popup_message("Your title must not be blank!") else: return input_data.strip() def show_popup_message(self, text): """Show pop-up messages for warning or displaying information in the GUI""" self.root.ids.popup_message.text = text self.root.ids.popup.open() def handle_close_popup(self): """Close the popup message""" self.root.ids.popup_message.text = '' self.root.ids.popup.dismiss() def handle_clear_button(self, is_add=False): """Clear the input when 'clear' button is pressed in the GUI """ if is_add: self.root.ids.title.text = '' self.root.ids.year.text = '' self.root.ids.artist.text = '' else: self.root.ids.title.text = '' self.root.ids.year.text = '' self.root.ids.artist.text = ''
def index_dj(request): template = get_template("Index.html") LOG_LEVEL = logging.DEBUG LOGFORMAT = "%(log_color)s%(message)s%(reset)s" logging.root.setLevel(LOG_LEVEL) formatter = ColoredFormatter(LOGFORMAT) stream = logging.StreamHandler() stream.setLevel(LOG_LEVEL) stream.setFormatter(formatter) logger = logging.getLogger('colorlogger') logger.setLevel(LOG_LEVEL) logger.addHandler(stream) sc = SongCollection() tl = TrackLister(sc) dj = DjController(tl) essentia.log.infoActive = False essentia.log.warningActive = False if request.method == "POST": cmd = request.POST.get("cmd", None) cmd = str(cmd) # cmd_split = str(cmd).split # cmd = cmd_split[0] while (True): # try: # cmd_split = str.split(input('> : '), ' ') # except KeyboardInterrupt: # logger.info('Goodbye!') # break # cmd = cmd_split[0] # if cmd == 'loaddir': # if len(cmd_split) == 1: # return HttpResponse('Please provide a directory name to load!') # continue # elif not os.path.isdir(cmd_split[1]): # return HttpResponse(cmd_split[1] + ' is not a valid directory!') # continue message = "abc" sc.load_directory("/home/ddman/音樂/upload") message = str(len(sc.songs)) + ' songs loaded [annotated: ' + str( len(sc.get_annotated())) + ']' if cmd == 'play': if len(sc.get_annotated()) == 0: message = 'Use the loaddir command to load some songs before playing!' continue # if len(cmd_split) > 1 and cmd_split[1] == 'save': # message = 'Saving this new mix to disk!' # save_mix = True # else: # save_mix = False message = 'Starting playback!' try: dj.play(save_mix=False) except Exception as e: logger.error(e) return render(request, "Index.html", locals()) elif cmd == 'pause': message = 'Pausing playback!' try: dj.pause() except Exception as e: logger.error(e) return render(request, "Index.html", locals()) elif cmd == 'skip' or cmd == 's': message = 'Skipping to next segment...' try: dj.skipToNextSegment() except Exception as e: logger.error(e) return render(request, "Index.html", locals()) elif cmd == 'stop': message = 'Stopping playback!' dj.stop() return render(request, "Index.html", locals()) elif cmd == 'save': message = 'Saving the next new mix!' return render(request, "Index.html", locals()) elif cmd == 'showannotated': message = 'Number of annotated songs ' + str( len(sc.get_annotated())) message = 'Number of unannotated songs ' + str( len(sc.get_unannotated())) return render(request, "Index.html", locals()) elif cmd == 'annotate': message = 'Started annotating!' sc.annotate() message = 'Done annotating!' return render(request, "Index.html", locals()) elif cmd == 'debug': LOG_LEVEL = logging.DEBUG logging.root.setLevel(LOG_LEVEL) stream.setLevel(LOG_LEVEL) logger.setLevel(LOG_LEVEL) message = 'Enabled debug info. Use this command before playing, or it will have no effect.' return render(request, "Index.html", locals()) elif cmd == 'mark': dj.markCurrentMaster() return render(request, "Index.html", locals()) elif cmd == "quit": break return render(request, "Index.html", locals()) else: message = 'The command ' + str(cmd) + ' does not exist!' return render(request, "Index.html", locals()) return render(request, "Index.html", locals())
features.extend(skew) #~ features.extend(kurt) # Does not help, even makes accuracy worse features.extend(avg_d) features.extend(std_d) #~ features.extend(skew_d) # Does not contribute a lot add_moment_features(pool['spec_contrast']) add_moment_features(pool['spec_valley']) add_moment_features(pool['mfcc_coeff']) return np.array(features, dtype='single') if __name__ == '__main__': sc = SongCollection() for dir_ in sys.argv[1:]: sc.load_directory(dir_) # Read annotated files traindir = '../SingingVoiceDetection/' testdir = '../SingingVoiceDetection/test/' def get_songs_and_annots(csv_dir): songs = [] annotations = [] for filename in os.listdir(csv_dir): if filename.endswith('.csv'): title, ext = os.path.splitext(filename) matching_songs = [ s for s in sc.get_annotated() if s.title == title
#~ for i in range(0,len(master)-HOP+1,HOP): #~ m = np.sum(master[i:i+4]) >= 2 #~ s = np.sum( slave[i:i+4]) >= 2 #~ if m and s: #~ return True #~ return False # With median filtering #~ master = 2*master[1:-1] + master[:-2] + master[2:] >= 2 #~ slave = 2*slave[1:-1] + slave[:-2] + slave[2:] >= 2 return sum(np.logical_and(master, slave)) >= 2 if __name__ == '__main__': sc = SongCollection() for dir_ in sys.argv[1:]: sc.load_directory(dir_) def get_songs_and_annots(csv_dir): songs = [] annotations = [] for filename in os.listdir(csv_dir): if filename.endswith('.csv'): title, ext = os.path.splitext(filename) matching_songs = [ s for s in sc.get_annotated() if s.title == title ] if len(matching_songs) > 0: songs.extend(matching_songs) annot_cur = []
from essentia import * from essentia.standard import * import pyaudio import sklearn from sklearn.decomposition import PCA from sklearn import preprocessing import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np import scipy if __name__ == '__main__': sc = SongCollection() for dir_ in sys.argv[2:]: sc.load_directory(dir_) # Read annotated files traindir = '../SingingVoiceDetection/' testdir = '../SingingVoiceDetection/test/' #~ validatedir = TODO? wss makkelijker te implementeren def get_songs_and_annots(csv_dir): songs = [] annotations = [] for filename in os.listdir(csv_dir): if filename.endswith('.csv'): title, ext = os.path.splitext(filename) matching_songs = [
from songcollection import SongCollection import pyaudio, csv import numpy as np from essentia.standard import AudioOnsetsMarker if __name__ == '__main__': # Open the long library sc = SongCollection() sc.load_directory('../moremusic/') analysedSongs = [] with open('evaluateMoresongs.csv', 'a+') as csvfile: for row in csv.reader(csvfile): analysedSongs.append(row[0]) print analysedSongs with open('evaluateMoresongs.csv', 'a') as csvfile: writer = csv.writer(csvfile) p = pyaudio.PyAudio() stream = p.open(format=pyaudio.paFloat32, channels=1, rate=44100, output=True) if len(analysedSongs) == 0: initrow = [ 'Song title', 'Segment index (downbeats)', 'Is aligned', 'Is low-to-high' ]
class SongsToLearnApp(App): current_selection = StringProperty() spinner_menu = ListProperty() songs = SongCollection() song = Song() song_id_list = [] def build(self): SongCollection() # initialise an empty list on program start Window.size = (900, 600) self.title = 'Songs to Learn 2.0' self.root = Builder.load_file('app.kv') songs_list = self.load_songs() self.create_entry_grids(songs_list) self.root.ids.message_box.text = 'Welcome to Songs to Learn 2.0' return self.root def create_entry_grids(self, input_list): self.clear_widget() for song in input_list: if song.is_learned == True: learn_state = '(Learned)' btn_color = (0, 1, 0, 1) else: learn_state = '' btn_color = (1, 0, 0, 1) btn_id = '{}_{}'.format(song.title, song.year) temp_button = Button(id=btn_id, background_color=btn_color, text='"{}" by {} ({}) {}'.format( song.title, song.artist, song.year, learn_state)) self.root.ids.song_count.add_widget(temp_button) self.song_id_list.append(temp_button) temp_button.bind(on_release=self.handle_mark) self.song_status() def handle_clear(self): # Clears the text input fields to empty self.root.ids.input_title.text = '' self.root.ids.input_artist.text = '' self.root.ids.input_year.text = '' def handle_add(self): """Validates input, and passes the inputs to the function to create a new song object""" song_title = self.root.ids.input_title.text song_artist = self.root.ids.input_artist.text song_year = self.root.ids.input_year.text err_check = self.error_check() if err_check == 'Blank': self.root.ids.message_box.color = (1, 0, 0, 1) self.root.ids.message_box.text = 'All fields must be completed' self.root.ids.btn_add.background_color = (1, 0, 0, 1) elif err_check == 'Invalid': self.root.ids.message_box.color = (1, 0, 0, 1) self.root.ids.message_box.text = 'Year Invalid. Please enter Valid year (1800 to 2018)' self.root.ids.btn_add.background_color = (1, 0, 0, 1) elif err_check == 'Type': self.root.ids.message_box.color = (1, 0, 0, 1) self.root.ids.message_box.text = 'Error in Field "Year". Please Check Input' self.root.ids.btn_add.background_color = (1, 0, 0, 1) elif err_check == 'Duplicate': self.root.ids.message_box.color = (1, 0, 0, 1) self.root.ids.message_box.text = '{} by {} already exists'.format( song_title, song_artist) self.root.ids.btn_add.background_color = (1, 0, 0, 1) else: send_input = Song(song_title, song_artist, song_year, False) # Registers the song attribute given by the user, with additional attribute 'learned' automatically set to False added_list = self.songs.add_songs(send_input) self.make_entries(added_list) self.root.ids.message_box.color = (1, 1, 1, 1) self.root.ids.message_box.text = 'You added {} by {}'.format( song_title, song_artist) self.root.ids.btn_add.background_color = (0, 0, 1, 1) def handle_sort_change(self, spinner_choice): self.clear_widget() sort_attr = spinner_choice if sort_attr == 'Title': sorted_list = self.songs.sort_songs(0) elif sort_attr == 'Artist': sorted_list = self.songs.sort_songs(1) elif sort_attr == 'Year': sorted_list = self.songs.sort_songs(2) self.make_entries(sorted_list) self.root.ids.message_box.text = 'Sorted by {}'.format(sort_attr) def load_songs(self): sort_list = self.songs.load_songs('songs.csv') return sort_list def song_status(self): learn = self.songs.calculate_song_learned() not_learn = self.songs.calculate_song_not_learned() total_songs = learn + not_learn status_text = 'To learn:{}.is_learned:{}'.format(learn, not_learn) self.root.ids.status_bar.text = status_text def make_entries(self, input_list): self.clear_widget() for song in input_list: if song.is_learned == True: learn_state = '(Learned)' btn_color = (0, 1, 0, 1) else: learn_state = '' btn_color = (1, 0, 0, 1) btn_id = '{}_{}'.format(song.title, song.year) temp_button = Button(id=btn_id, background_color=btn_color, text='"{}" by {} ({}) {}'.format( song.title, song.artist, song.year, learn_state)) self.song_id_list.append(temp_button) self.root.ids.song_count.add_widget(temp_button) temp_button.bind(on_release=self.handle_mark) self.song_status() def handle_mark(self, instance): btn = instance.id for song in self.songs.songs: validate = '{}_{}'.format(song.title, song.year) if btn == validate: if song.is_learned == True: self.root.ids.message_box.text = 'You have already learned {} ({})'.format( song.title, song.year) else: self.songs.mark_as_learned(btn) self.root.ids.message_box.text = ( 'You learned {} ({})'.format(song.title, song.year)) give_list = self.songs.give_songs() self.make_entries(give_list) def exit_app(self): self.songs.save_songs() def clear_widget(self): for song_id in self.song_id_list: self.root.ids.song_count.remove_widget(song_id) def error_check(self): title_check = self.root.ids.input_title.text artist_check = self.root.ids.input_artist.text year_check = self.root.ids.input_year.text error_state = '' if title_check == '' or artist_check == '' or year_check == '': error_state = 'Blank' else: try: int(year_check) except ValueError: error_state = 'Type' else: if int(year_check) > 2020 or int(year_check) < 1800: error_state = 'Invalid' for song in self.songs.songs: if title_check == song.title and artist_check == song.artist and year_check == song.year: error_state = 'Duplicate' return error_state def handle_stop(self): self.songs.save_songs() sys.exit(0)
hop_size = 44100 * HOP_S pool = Pool() for idx in range(start_sample, stop_sample - length_samples, hop_size): print 'Analysed until {:.2f}'.format((idx/44100.0)/60) start_idx = idx end_idx = idx + length_samples fragment = audio[start_idx : end_idx] theme_descr = calculateThemeDescriptor(fragment) print theme_descr pool.add('theme_descriptors', theme_descr.tolist()[0]) # --------------------- Load all audio files --------------------------- from songcollection import SongCollection sc = SongCollection() sc.load_directory('../music') sc.load_directory('../moremusic') sc.load_directory('../evenmoremusic') sc.load_directory('../music/test') songs = [] for song in sc.get_annotated(): song.open() pool.add('song.themes', song.song_theme_descriptor.tolist()[0]) songs.append(song.title) song.close() # --------------------- Make nice plots --------------------------- Y = pool['song.themes'] # All songs in "/music" and "/moremusic" libraries
""" Name: Nguyen Quoc Minh Quan Date: 22/5/2020 Brief Project Description: My CP1404 final assignment working with classes and Kivy, i have the chance to re-create my assignment 1 with classes and create GUI with Kivy GitHub URL: https://github.com/JCUS-CP1404/assignment-02-songs-app-minhquan0902 """ # TODO: Copy your first assignment to this file, commit, then update to use Song class from song import Song import csv from songcollection import SongCollection my_song = SongCollection() my_song.load_songs('songs.csv') def main(): """Main function of the program""" print("Songs to Learn 1.0 - by Quan Nguyen") print("{} songs loaded".format(len(my_song.song_list))) print("\n") flag = False while not flag: print("Menu: ") print("L - List songs") print("A - Add new song") print("S - Search a song") print("C - Complete a song") print("Q - Quit")