diff --git a/VocalRemover.py b/VocalRemover.py index ea811e1..01a79f3 100644 --- a/VocalRemover.py +++ b/VocalRemover.py @@ -4,6 +4,7 @@ import tkinter.ttk as ttk import tkinter.messagebox import tkinter.filedialog import tkinter.font +from tkinterdnd2 import TkinterDnD, DND_FILES # Enable Drag & Drop from datetime import datetime # Images from PIL import Image @@ -93,6 +94,7 @@ def open_image(path: str, size: tuple = None, keep_aspect: bool = True, rotate: def save_data(data): """ Saves given data as a .pkl (pickle) file + Paramters: data(dict): Dictionary containing all the necessary data to save @@ -105,6 +107,7 @@ def save_data(data): def load_data() -> dict: """ Loads saved pkl file and returns the stored data + Returns(dict): Dictionary containing all the saved data """ @@ -166,6 +169,33 @@ def get_model_values(model_name): return model_values +def drop(event, accept_mode: str = 'files'): + """ + Drag & Drop verification process + """ + path = event.data + + if accept_mode == 'folder': + path = path.replace('{', '').replace('}', '') + if not os.path.isdir(path): + tk.messagebox.showerror(title='Invalid Folder', + message='Your given export path is not a valid folder!') + return + # Set Variables + root.exportPath_var.set(path) + elif accept_mode == 'files': + # Clean path text and set path to the list of paths + path = path[:-1] + path = path.replace('{', '') + path = path.split('} ') + # Set Variables + root.inputPaths = path + root.inputPathsEntry_var.set('; '.join(path)) + else: + # Invalid accept mode + return + + class ThreadSafeConsole(tk.Text): """ Text Widget which is thread safe for tkinter @@ -199,7 +229,7 @@ class ThreadSafeConsole(tk.Text): self.after(100, self.update_me) -class MainWindow(tk.Tk): +class MainWindow(TkinterDnD.Tk): # --Constants-- # Layout IMAGE_HEIGHT = 140 @@ -231,6 +261,7 @@ class MainWindow(tk.Tk): xpad=int(self.winfo_screenwidth()/2 - 550/2), ypad=int(self.winfo_screenheight()/2 - height/2 - 30))) self.configure(bg='#000000') # Set background color to black + self.protocol("WM_DELETE_WINDOW", self.save_values) self.resizable(False, False) self.update() @@ -271,6 +302,7 @@ class MainWindow(tk.Tk): self.aiModel_var = tk.StringVar(value=data['aiModel']) self.last_aiModel = self.aiModel_var.get() # Other + self.inputPathsEntry_var = tk.StringVar(value='') self.lastDir = data['lastDir'] # nopep8 self.progress_var = tk.IntVar(value=0) # Font @@ -278,6 +310,7 @@ class MainWindow(tk.Tk): # --Widgets-- self.create_widgets() self.configure_widgets() + self.bind_widgets() self.place_widgets() self.update_available_models() @@ -319,6 +352,21 @@ class MainWindow(tk.Tk): font=self.font, foreground='white') ttk.Style().configure('T', font=self.font, foreground='white') + def bind_widgets(self): + """Bind widgets to the drag & drop mechanic""" + self.filePaths_saveTo_Button.drop_target_register(DND_FILES) + self.filePaths_saveTo_Entry.drop_target_register(DND_FILES) + self.filePaths_musicFile_Button.drop_target_register(DND_FILES) + self.filePaths_musicFile_Entry.drop_target_register(DND_FILES) + self.filePaths_saveTo_Button.dnd_bind('<>', + lambda e: drop(e, accept_mode='folder')) + self.filePaths_saveTo_Entry.dnd_bind('<>', + lambda e: drop(e, accept_mode='folder')) + self.filePaths_musicFile_Button.dnd_bind('<>', + lambda e: drop(e, accept_mode='files')) + self.filePaths_musicFile_Entry.dnd_bind('<>', + lambda e: drop(e, accept_mode='files')) + def place_widgets(self): """Place main widgets""" self.title_Label.place(x=-2, y=-2) @@ -352,7 +400,7 @@ class MainWindow(tk.Tk): text='Select Your Audio File(s)', command=self.open_file_filedialog) self.filePaths_musicFile_Entry = ttk.Entry(master=self.filePaths_Frame, - text=self.inputPaths, + textvariable=self.inputPathsEntry_var, state=tk.DISABLED ) # -Place Widgets- @@ -556,12 +604,7 @@ class MainWindow(tk.Tk): ) if paths: # Path selected self.inputPaths = paths - # Change the entry text - self.filePaths_musicFile_Entry.configure(state=tk.NORMAL) - self.filePaths_musicFile_Entry.delete(0, tk.END) - self.filePaths_musicFile_Entry.insert(0, self.inputPaths) - self.filePaths_musicFile_Entry.configure(state=tk.DISABLED) - + self.inputPathsEntry_var.set('; '.join(paths)) self.lastDir = os.path.dirname(paths[0]) def open_export_filedialog(self): @@ -582,6 +625,7 @@ class MainWindow(tk.Tk): """ # -Get all variables- export_path = self.exportPath_var.get() + input_paths = self.inputPaths instrumentalModel_path = self.instrumentalLabel_to_path[self.instrumentalModel_var.get()] # nopep8 stackedModel_path = self.stackedLabel_to_path[self.stackedModel_var.get()] # nopep8 # Get constants @@ -611,12 +655,13 @@ class MainWindow(tk.Tk): return # -Check for invalid inputs- - if not any([(os.path.isfile(path) and path.endswith(('.mp3', '.mp4', '.m4a', '.flac', '.wav'))) - for path in self.inputPaths]): - tk.messagebox.showwarning(master=self, - title='Invalid Music File', - message='You have selected an invalid music file!\nPlease make sure that your files still exist and ends with either ".mp3", ".mp4", ".m4a", ".flac", ".wav"') - return + for path in input_paths: + if not os.path.isfile(path): + tk.messagebox.showwarning(master=self, + title='Invalid Music File', + message='You have selected an invalid music file! Please make sure that the file still exists!', + detail=f'File path: {path}') + return if not os.path.isdir(export_path): tk.messagebox.showwarning(master=self, title='Invalid Export Directory', @@ -636,27 +681,6 @@ class MainWindow(tk.Tk): message='You have selected an invalid stacked model file!\nPlease make sure that your model file still exists!') return - # -Save Data- - save_data(data={ - 'export_path': export_path, - 'gpu': self.gpuConversion_var.get(), - 'postprocess': self.postprocessing_var.get(), - 'tta': self.tta_var.get(), - 'output_image': self.outputImage_var.get(), - 'stack': self.stack_var.get(), - 'stackOnly': self.stackOnly_var.get(), - 'stackPasses': stackPasses, - 'saveAllStacked': self.saveAllStacked_var.get(), - 'sr': sr, - 'hop_length': hop_length, - 'window_size': window_size, - 'n_fft': n_fft, - 'useModel': 'instrumental', # Always instrumental - 'lastDir': self.lastDir, - 'modelFolder': self.modelFolder_var.get(), - 'aiModel': self.aiModel_var.get(), - }) - if self.aiModel_var.get() == 'v2': inference = inference_v2 elif self.aiModel_var.get() == 'v4': @@ -668,7 +692,7 @@ class MainWindow(tk.Tk): threading.Thread(target=inference.main, kwargs={ # Paths - 'input_paths': self.inputPaths, + 'input_paths': input_paths, 'export_path': export_path, # Processing Options 'gpu': 0 if self.gpuConversion_var.get() else -1, @@ -724,20 +748,19 @@ class MainWindow(tk.Tk): # Loop through each constant (key) and its widgets for key, (widget, var) in widgetsVars.items(): if stacked_selectable: - # Stacked model can be selected - if key in stacked.keys(): - if (key in stacked.keys() and - not instrumental_selectable): - # Only stacked selectable - widget.configure(state=tk.DISABLED) - var.set(stacked[key]) - continue - elif (key in instrumental.keys() and - instrumental_selectable): + if instrumental_selectable: + if (key in instrumental.keys() and + key in stacked.keys()): # Both models have set constants widget.configure(state=tk.DISABLED) var.set('%d/%d' % (instrumental[key], stacked[key])) continue + else: + if key in stacked.keys(): + # Only stacked selectable + widget.configure(state=tk.DISABLED) + var.set(stacked[key]) + continue else: # Stacked model can not be selected if (key in instrumental.keys() and @@ -835,7 +858,6 @@ class MainWindow(tk.Tk): # Instrumental Model self.options_instrumentalModel_Label.configure(foreground='#000') self.options_instrumentalModel_Optionmenu.configure(state=tk.NORMAL) # nopep8 - self.instrumentalModel_var.set('') # Stack Model if stackLoops > 0: @@ -883,11 +905,54 @@ class MainWindow(tk.Tk): """ Restart the application after asking for confirmation """ - proceed = tk.messagebox.askyesno(title='Confirmation', - message='The application will restart and lose unsaved data. Do you wish to proceed?') - if proceed: - subprocess.Popen(f'python "{__file__}"', shell=True) - exit() + save = tk.messagebox.askyesno(title='Confirmation', + message='The application will restart. Do you want to save the data?') + if save: + self.save_values() + subprocess.Popen(f'python "{__file__}"', shell=True) + exit() + + def save_values(self): + """ + Save the data of the application + """ + export_path = self.exportPath_var.get() + # Get constants + instrumental = get_model_values(self.instrumentalModel_var.get()) + stacked = get_model_values(self.stackedModel_var.get()) + if [bool(instrumental), bool(stacked)].count(True) == 2: + sr = DEFAULT_DATA['sr'] + hop_length = DEFAULT_DATA['hop_length'] + window_size = DEFAULT_DATA['window_size'] + n_fft = DEFAULT_DATA['n_fft'] + else: + sr = self.srValue_var.get() + hop_length = self.hopValue_var.get() + window_size = self.winSize_var.get() + n_fft = self.nfft_var.get() + + # -Save Data- + save_data(data={ + 'export_path': export_path, + 'gpu': self.gpuConversion_var.get(), + 'postprocess': self.postprocessing_var.get(), + 'tta': self.tta_var.get(), + 'output_image': self.outputImage_var.get(), + 'stack': self.stack_var.get(), + 'stackOnly': self.stackOnly_var.get(), + 'stackPasses': self.stackLoops_var.get(), + 'saveAllStacked': self.saveAllStacked_var.get(), + 'sr': sr, + 'hop_length': hop_length, + 'window_size': window_size, + 'n_fft': n_fft, + 'useModel': 'instrumental', + 'lastDir': self.lastDir, + 'modelFolder': self.modelFolder_var.get(), + 'aiModel': self.aiModel_var.get(), + }) + + self.destroy() if __name__ == "__main__":