diff --git a/src/app.py b/src/app.py index 219bd22..f697841 100644 --- a/src/app.py +++ b/src/app.py @@ -57,7 +57,7 @@ class CustomApplication(QtWidgets.QApplication): # -Create Managers- self.logger = Logger() self.settings = QtCore.QSettings(const.APPLICATION_SHORTNAME, const.APPLICATION_NAME) - #self.settings.clear() + # self.settings.clear() self.resources = ResourcePaths() self.translator = Translator(self) self.themeManager = ThemeManager(self) diff --git a/src/inference/converter.py b/src/inference/converter.py index 5fc361f..0fb00c7 100644 --- a/src/inference/converter.py +++ b/src/inference/converter.py @@ -1,3 +1,33 @@ +# pylint: disable=no-name-in-module, import-error +# -GUI (Threads)- +from PySide2 import QtCore # (QRunnable, QThread, QObject, Signal, Slot) +from PySide2 import QtWidgets +# Multithreading +import threading +from multiprocessing import Pool +# -Required for conversion- +import cv2 +import librosa +import audioread +import numpy as np +import soundfile as sf +import torch +# -Root imports- +from .lib import dataset +from .lib import nets +from .lib import spec_utils +from ..resources.resources_manager import (ResourcePaths, Logger) +# -Other- +import traceback +# Loading Bar +from tqdm import tqdm +# Timer +import datetime as dt +import time +import os +# Annotating +from typing import (Dict, Tuple, Optional, Callable) + default_data = { # Paths 'input_paths': [], # List of paths @@ -23,3 +53,799 @@ default_data = { 'save_instrumentals': True, 'save_vocals': True, } + +class VocalRemover: + def __init__(self, seperation_data: dict, logger: Optional[Logger] = None): + # -Universal Data (Same for each file)- + self.seperation_data = seperation_data + self.general_data = { + 'total_loops': None, + 'folder_path': None, + 'file_add_on': None, + } + self.logger = logger + self.models = {} + self.devices = {} + # Threads + self.all_threads = [] + # -File Specific Data (Different for each file)- + # Updated on every conversion or loop + self.loop_data = { + # File specific + 'file_base_name': None, + 'file_num': 0, + # Loop specific + 'command_base_text': None, + 'loop_num': 0, + 'progress_step': 0.0, + 'music_file': None, + 'model_device': { + 'model': None, + 'device': None, + 'model_name': None, + }, + 'constants': { + 'sr': None, + 'hop_length': None, + 'window_size': None, + 'n_fft': None, + }, + 'X': None, + 'X_mag': None, + 'X_phase': None, + 'prediction': None, + 'sampling_rate': None, + 'wav_vocals': None, + 'wav_instrument': None, + 'y_spec': None, + 'v_spec': None, + # Spectogram from last seperation + 'temp_spectogramm': None, + } + # Needed for embedded audio player (GUI) + self.latest_instrumental_path: str + self.latest_vocal_path: str + + def seperate_files(self): + """ + Seperate all files + """ + # Track time + stime = time.perf_counter() + self._check_for_valid_inputs() + self._fill_general_data() + self.all_threads = [] + + for file_num, file_path in enumerate(self.seperation_data['input_paths'], start=1): + if self.seperation_data['multithreading']: + thread = threading.Thread(target=self._seperate, args=(file_path, file_num), + daemon=True) + thread.start() + self.all_threads.append(thread) + else: + self._seperate(file_path, + file_num) + + for thread in self.all_threads: + thread.join() + # Free RAM + torch.cuda.empty_cache() + + self.logger.info('Conversion(s) Completed and Saving all Files!') + self.logger.info(f'Time Elapsed: {time.strftime("%H:%M:%S", time.gmtime(int(time.perf_counter() - stime)))}') + + def write_to_gui(self, text: Optional[str] = None, include_base_text: bool = True, progress_step: Optional[float] = None): + """ + Update progress and/or write text to the command line + + A new line '\\n' will be automatically appended to the text + """ + self.logger.info(text) + print(text) + + def _fill_general_data(self): + """ + Fill the data implemented in general_data + """ + def get_folderPath_fileAddOn() -> Tuple[str, str]: + """ + Get export path and text, whic hwill be appended on the music files name + """ + file_add_on = '' + if self.seperation_data['modelFolder']: + # Model Test Mode selected + # -Instrumental- + if os.path.isfile(self.seperation_data['instrumentalModel']): + file_add_on += os.path.splitext(os.path.basename(self.seperation_data['instrumentalModel']))[0] + # -Vocal- + elif os.path.isfile(self.seperation_data['vocalModel']): + file_add_on += os.path.splitext(os.path.basename(self.seperation_data['vocalModel']))[0] + # -Stack- + if os.path.isfile(self.seperation_data['stackModel']): + file_add_on += '-' + os.path.splitext(os.path.basename(self.seperation_data['stackModel']))[0] + + # Generate paths + folder_path = os.path.join(self.seperation_data['export_path'], file_add_on) + file_add_on = f'_{file_add_on}' + + if not os.path.isdir(folder_path): + # Folder does not exist + os.mkdir(folder_path) + else: + # Not Model Test Mode selected + folder_path = self.seperation_data['export_path'] + + return folder_path, file_add_on + + def get_models_devices() -> list: + """ + Return models and devices found + """ + models = {} + devices = {} + + # -Instrumental- + if os.path.isfile(self.seperation_data['instrumentalModel']): + device = torch.device('cpu') + model = nets.CascadedASPPNet(self.seperation_data['n_fft']) + model.load_state_dict(torch.load(self.seperation_data['instrumentalModel'], + map_location=device)) + if torch.cuda.is_available() and self.seperation_data['gpuConversion']: + device = torch.device('cuda:0') + model.to(device) + + models['instrumental'] = model + devices['instrumental'] = device + # -Vocal- + elif os.path.isfile(self.seperation_data['vocalModel']): + device = torch.device('cpu') + model = nets.CascadedASPPNet(self.seperation_data['n_fft']) + model.load_state_dict(torch.load(self.seperation_data['vocalModel'], + map_location=device)) + if torch.cuda.is_available() and self.seperation_data['gpuConversion']: + device = torch.device('cuda:0') + model.to(device) + + models['vocal'] = model + devices['vocal'] = device + # -Stack- + if os.path.isfile(self.seperation_data['stackModel']): + device = torch.device('cpu') + model = nets.CascadedASPPNet(self.seperation_data['n_fft']) + model.load_state_dict(torch.load(self.seperation_data['stackModel'], + map_location=device)) + if torch.cuda.is_available() and self.seperation_data['gpuConversion']: + device = torch.device('cuda:0') + model.to(device) + + models['stack'] = model + devices['stack'] = device + + return models, devices + + def get_total_loops() -> int: + """ + Determine how many loops the program will + have to prepare for + """ + if self.seperation_data['stackOnly']: + # Stack Conversion Only + total_loops = self.seperation_data['stackPasses'] + else: + # 1 for the instrumental/vocal + total_loops = 1 + # Add number of stack pass loops + total_loops += self.seperation_data['stackPasses'] + + return total_loops + + # -Get data- + total_loops = get_total_loops() + folder_path, file_add_on = get_folderPath_fileAddOn() + self.logger.info('Loading models...') + models, devices = get_models_devices() + + # -Set data- + self.general_data['total_files'] = len(self.seperation_data['input_paths']) + self.general_data['total_loops'] = total_loops + self.general_data['folder_path'] = folder_path + self.general_data['file_add_on'] = file_add_on + self.models = models + self.devices = devices + + def _check_for_valid_inputs(self): + """ + Check if all inputs have been entered correctly. + + If errors are found, an exception is raised + """ + # Check input paths + if not len(self.seperation_data['input_paths']): + # No music file specified + raise Exception('No music file to seperate defined!') + if (not isinstance(self.seperation_data['input_paths'], tuple) and + not isinstance(self.seperation_data['input_paths'], list)): + # Music file not specified in a list or tuple + raise Exception('Please specify your music file path/s in a list or tuple!') + for input_path in self.seperation_data['input_paths']: + # Go through each music file + if not os.path.isfile(input_path): + # Invalid path + raise Exception(f'Invalid music file! Please make sure that the file still exists or that the path is valid!\nPath: "{input_path}"') # nopep8 + # Output path + if (not os.path.isdir(self.seperation_data['export_path']) and + not self.seperation_data['export_path'] == ''): + # Export path either invalid or not specified + raise Exception(f'Invalid export directory! Please make sure that the directory still exists or that the path is valid!\nPath: "{self.seperation_data["export_path"]}"') # nopep8 + + # Check models + if not self.seperation_data['useModel'] in ['vocal', 'instrumental']: + # Invalid 'useModel' + raise Exception("Parameter 'useModel' has to be either 'vocal' or 'instrumental'") + if not os.path.isfile(self.seperation_data[f"{self.seperation_data['useModel']}Model"]): + # No or invalid instrumental/vocal model given + # but model is needed + raise Exception(f"Not specified or invalid model path for {self.seperation_data['useModel']} model!") + if (self.seperation_data['stackOnly'] or + self.seperation_data['stackPasses'] > 0): + # First check if stacked model is needed + if not os.path.isfile(self.seperation_data['stackModel']): + # No or invalid stack model given + # but model is needed + raise Exception(f"Not specified or invalid model path for stacked model!") + + def _seperate(self, file_path: str, file_num: int): + """ + Seperate given music file, + file_num is used to determine progress + """ + + # -Update file specific variables- + self.loop_data['file_num'] = file_num + self.loop_data['music_file'] = file_path + self.loop_data['file_base_name'] = self._get_file_base_name(file_path) + + for loop_num in range(self.general_data['total_loops']): + self.loop_data['loop_num'] = loop_num + # -Get loop specific variables- + command_base_text = self._get_base_text() + model_device = self._get_model_device_file() + constants = self._get_constants(model_device['model_name']) + # -Update loop specific variables + self.loop_data['constants'] = constants + self.loop_data['command_base_text'] = command_base_text + self.loop_data['model_device'] = model_device + + # -Seperation- + if not self.loop_data['loop_num']: + # First loop + self._load_wave_source() + self._wave_to_spectogram() + if self.seperation_data['postProcess']: + # Postprocess + self._post_process() + self._inverse_stft_of_instrumentals_and_vocals() + self._save_files() + else: + # End of seperation + if self.seperation_data['outputImage']: + self._save_mask() + + self.write_to_gui(text='Completed Seperation!\n', + progress_step=1) + + # -Data Getter Methods- + def _get_base_text(self) -> str: + """ + Determine the prefix text of the console + """ + loop_add_on = '' + if self.general_data['total_loops'] > 1: + # More than one loop for conversion + loop_add_on = f" ({self.loop_data['loop_num']+1}/{self.general_data['total_loops']})" + + return 'File {file_num}/{total_files}:{loop} '.format(file_num=self.loop_data['file_num'], + total_files=self.general_data['total_files'], + loop=loop_add_on) + + def _get_constants(self, model_name: str) -> dict: + """ + Get the sr, hop_length, window_size, n_fft + """ + if self.loop_data['loop_num'] == 0: + # Instrumental/Vocal Model + seperation_params = { + 'sr': self.seperation_data['sr_stacked'], + 'hop_length': self.seperation_data['hop_length_stacked'], + 'window_size': self.seperation_data['window_size_stacked'], + 'n_fft': self.seperation_data['n_fft_stacked'], + } + else: + # Stacked model + seperation_params = { + 'sr': self.seperation_data['sr'], + 'hop_length': self.seperation_data['hop_length'], + 'window_size': self.seperation_data['window_size'], + 'n_fft': self.seperation_data['n_fft'], + } + if self.seperation_data['customParameters']: + # Typed constants are fixed + return seperation_params + + # -Decode Model Name- + text = model_name.replace('.pth', '') + text_parts = text.split('_')[1:] + for text_part in text_parts: + if 'sr' in text_part: + text_part = text_part.replace('sr', '') + if text_part.isdecimal(): + try: + seperation_params['sr'] = int(text_part) + continue + except ValueError: + # Cannot convert string to int + pass + if 'hl' in text_part: + text_part = text_part.replace('hl', '') + if text_part.isdecimal(): + try: + seperation_params['hop_length'] = int(text_part) + continue + except ValueError: + # Cannot convert string to int + pass + if 'w' in text_part: + text_part = text_part.replace('w', '') + if text_part.isdecimal(): + try: + seperation_params['window_size'] = int(text_part) + continue + except ValueError: + # Cannot convert string to int + pass + if 'nf' in text_part: + text_part = text_part.replace('nf', '') + if text_part.isdecimal(): + try: + seperation_params['n_fft'] = int(text_part) + continue + except ValueError: + # Cannot convert string to int + pass + + return seperation_params + + def _get_model_device_file(self) -> dict: + """ + Get the used models and devices for this loop + Also extract the model name and the music file + which will be used + """ + model_device = { + 'model': None, + 'device': None, + 'model_name': None, + } + if self.seperation_data['stackOnly']: + # Stack Only Conversion + if os.path.isfile(self.seperation_data['stackModel']): + model_device['model'] = self.models['stack'] + model_device['device'] = self.devices['stack'] + model_device['model_name'] = os.path.basename(self.seperation_data['stackModel']) + else: + raise ValueError(f'Selected stack only model, however, stack model path file cannot be found\nPath: "{self.seperation_data["stackModel"]}"') # nopep8 + elif not self.loop_data['loop_num']: + # First Loop + model_device['model'] = self.models[self.seperation_data['useModel']] + model_device['device'] = self.devices[self.seperation_data['useModel']] + model_device['model_name'] = os.path.basename( + self.seperation_data[f'{self.seperation_data["useModel"]}Model']) + else: + # Every other iteration + model_device['model'] = self.models['stack'] + model_device['device'] = self.devices['stack'] + model_device['model_name'] = os.path.basename(self.seperation_data['stackModel']) + + return model_device + + def _get_file_base_name(self, file_path: str) -> str: + """ + Get the path infos for the given music file + """ + return f"{self.loop_data['file_num']}_{os.path.splitext(os.path.basename(file_path))[0]}" + + # -Seperation Methods- + def _load_wave_source(self): + """ + Load the wave source + """ + self.write_to_gui(text='Loading wave source...', + progress_step=0) + try: + X, sampling_rate = librosa.load(path=self.loop_data['music_file'], + sr=self.loop_data['constants']['sr'], + mono=False, dtype=np.float32, + res_type=self.seperation_data['resType']) + except audioread.NoBackendError: + raise Exception( + f'Invalid music file provided! Please check its validity.\nFile: "{self.loop_data["music_file"]}"') + + if X.ndim == 1: + X = np.asarray([X, X]) + + self.loop_data['X'] = X + self.loop_data['sampling_rate'] = sampling_rate + + def _wave_to_spectogram(self): + """ + Wave to spectogram + """ + def preprocess(X_spec): + X_mag = np.abs(X_spec) + X_phase = np.angle(X_spec) + + return X_mag, X_phase + + def execute(X_mag_pad, roi_size, n_window, device, model, progrs_info: str = ''): + model.eval() + with torch.no_grad(): + preds = [] + bar_format = '{desc} |{bar}{r_bar}' + pbar = tqdm(range(n_window), bar_format=bar_format) + + for progrs, i in enumerate(pbar): + # Progress management + if progrs_info == '1/2': + progres_step = 0.1 + 0.35 * (progrs / n_window) + elif progrs_info == '2/2': + progres_step = 0.45 + 0.35 * (progrs / n_window) + else: + progres_step = 0.1 + 0.7 * (progrs / n_window) + self.write_to_gui(progress_step=progres_step) + + progress = self._get_progress(progres_step) + text = f'{int(progress)} %' + if progress < 10: + text += ' ' + pbar.set_description_str(text) + + start = i * roi_size + X_mag_window = X_mag_pad[None, :, :, + start:start + self.seperation_data['window_size']] + X_mag_window = torch.from_numpy(X_mag_window).to(device) + + pred = model.predict(X_mag_window) + + pred = pred.detach().cpu().numpy() + preds.append(pred[0]) + + pred = np.concatenate(preds, axis=2) + + return pred + + def inference(X_spec, device, model): + X_mag, X_phase = preprocess(X_spec) + + coef = X_mag.max() + X_mag_pre = X_mag / coef + + n_frame = X_mag_pre.shape[2] + pad_l, pad_r, roi_size = dataset.make_padding(n_frame, + self.seperation_data['window_size'], model.offset) + n_window = int(np.ceil(n_frame / roi_size)) + + X_mag_pad = np.pad( + X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant') + + pred = execute(X_mag_pad, roi_size, n_window, + device, model) + pred = pred[:, :, :n_frame] + + return pred * coef, X_mag, np.exp(1.j * X_phase) + + def inference_tta(X_spec, device, model): + X_mag, X_phase = preprocess(X_spec) + + coef = X_mag.max() + X_mag_pre = X_mag / coef + + n_frame = X_mag_pre.shape[2] + pad_l, pad_r, roi_size = dataset.make_padding(n_frame, + self.seperation_data['window_size'], model.offset) + n_window = int(np.ceil(n_frame / roi_size)) + + X_mag_pad = np.pad( + X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant') + + pred = execute(X_mag_pad, roi_size, n_window, + device, model, progrs_info='1/2') + pred = pred[:, :, :n_frame] + + pad_l += roi_size // 2 + pad_r += roi_size // 2 + n_window += 1 + + X_mag_pad = np.pad( + X_mag_pre, ((0, 0), (0, 0), (pad_l, pad_r)), mode='constant') + + pred_tta = execute(X_mag_pad, roi_size, n_window, + device, model, progrs_info='2/2') + pred_tta = pred_tta[:, :, roi_size // 2:] + pred_tta = pred_tta[:, :, :n_frame] + + return (pred + pred_tta) * 0.5 * coef, X_mag, np.exp(1.j * X_phase) + + self.write_to_gui(text='Stft of wave source...', + progress_step=0.1) + + if not self.loop_data['loop_num']: + X = spec_utils.wave_to_spectrogram(wave=self.loop_data['X'], + hop_length=self.seperation_data['hop_length'], + n_fft=self.seperation_data['n_fft']) + else: + X = self.loop_data['temp_spectogramm'] + + if self.seperation_data['tta']: + prediction, X_mag, X_phase = inference_tta(X_spec=X, + device=self.loop_data['model_device']['device'], + model=self.loop_data['model_device']['model']) + else: + prediction, X_mag, X_phase = inference(X_spec=X, + device=self.loop_data['model_device']['device'], + model=self.loop_data['model_device']['model']) + + self.loop_data['prediction'] = prediction + self.loop_data['X'] = X + self.loop_data['X_mag'] = X_mag + self.loop_data['X_phase'] = X_phase + + def _post_process(self): + """ + Post process + """ + self.write_to_gui(text='Post processing...', + progress_step=0.8) + + pred_inv = np.clip(self.loop_data['X_mag'] - self.loop_data['prediction'], 0, np.inf) + prediction = spec_utils.mask_silence(self.loop_data['prediction'], pred_inv) + + self.loop_data['prediction'] = prediction + + def _inverse_stft_of_instrumentals_and_vocals(self): + """ + Inverse stft of instrumentals and vocals + """ + self.write_to_gui(text='Inverse stft of instruments and vocals...', + progress_step=0.85) + + y_spec = self.loop_data['prediction'] * self.loop_data['X_phase'] + v_spec = np.clip(self.loop_data['X_mag'] - self.loop_data['prediction'], + 0, np.inf) * self.loop_data['X_phase'] + + if self.loop_data['loop_num'] == (self.general_data['total_loops'] - 1): + # Only compute wave on last loop + wav_instrument = spec_utils.spectrogram_to_wave(y_spec, + hop_length=self.seperation_data['hop_length']) + self.loop_data['wav_instrument'] = wav_instrument + wav_vocals = spec_utils.spectrogram_to_wave(v_spec, + hop_length=self.seperation_data['hop_length']) + self.loop_data['wav_vocals'] = wav_vocals + + # Needed for mask creation + self.loop_data['y_spec'] = y_spec + self.loop_data['v_spec'] = v_spec + + self.loop_data['temp_spectogramm'] = y_spec + + def _save_files(self): + """ + Save the files + """ + def get_vocal_instrumental_name() -> Tuple[str, str, str]: + """ + Get vocal and instrumental file names and update the + folder_path temporarily if needed + """ + loop_num = self.loop_data['loop_num'] + total_loops = self.general_data['total_loops'] + file_base_name = self.loop_data['file_base_name'] + vocal_name = None + instrumental_name = None + folder_path = self.general_data['folder_path'] + + # Get the Suffix Name + if (not loop_num or + loop_num == (total_loops - 1)): # First or Last Loop + if self.seperation_data['stackOnly']: + if loop_num == (total_loops - 1): # Last Loop + if not (total_loops - 1): # Only 1 Loop + vocal_name = '(Vocals)' + instrumental_name = '(Instrumental)' + else: + vocal_name = '(Vocal_Final_Stacked_Output)' + instrumental_name = '(Instrumental_Final_Stacked_Output)' + elif self.seperation_data['useModel'] == 'instrumental': + if not loop_num: # First Loop + vocal_name = '(Vocals)' + if loop_num == (total_loops - 1): # Last Loop + if not (total_loops - 1): # Only 1 Loop + instrumental_name = '(Instrumental)' + else: + instrumental_name = '(Instrumental_Final_Stacked_Output)' + elif self.seperation_data['useModel'] == 'vocal': + if not loop_num: # First Loop + instrumental_name = '(Instrumental)' + if loop_num == (total_loops - 1): # Last Loop + if not (total_loops - 1): # Only 1 Loop + vocal_name = '(Vocals)' + else: + vocal_name = '(Vocals_Final_Stacked_Output)' + if self.seperation_data['useModel'] == 'vocal': + # Reverse names + vocal_name, instrumental_name = instrumental_name, vocal_name + elif self.seperation_data['saveAllStacked']: + stacked_folder_name = file_base_name + ' Stacked Outputs' # nopep8 + folder_path = os.path.join(folder_path, stacked_folder_name) + + if not os.path.isdir(folder_path): + os.mkdir(folder_path) + + if self.seperation_data['stackOnly']: + vocal_name = f'(Vocal_{loop_num}_Stacked_Output)' + instrumental_name = f'(Instrumental_{loop_num}_Stacked_Output)' + elif (self.seperation_data['useModel'] == 'vocal' or + self.seperation_data['useModel'] == 'instrumental'): + vocal_name = f'(Vocals_{loop_num}_Stacked_Output)' + instrumental_name = f'(Instrumental_{loop_num}_Stacked_Output)' + + if self.seperation_data['useModel'] == 'vocal': + # Reverse names + vocal_name, instrumental_name = instrumental_name, vocal_name + return vocal_name, instrumental_name, folder_path + + self.write_to_gui(text='Saving Files...', + progress_step=0.9) + + vocal_name, instrumental_name, folder_path = get_vocal_instrumental_name() + + # -Save files- + instrumental_file_name = f"{self.loop_data['file_base_name']}_{instrumental_name}{self.general_data['file_add_on']}.wav" + vocal_file_name = f"{self.loop_data['file_base_name']}_{vocal_name}{self.general_data['file_add_on']}.wav" + self.latest_instrumental_path = os.path.join(folder_path, + instrumental_file_name) + self.latest_vocal_path = os.path.join(folder_path, + vocal_file_name) + # Instrumental + if (instrumental_name is not None and + self.seperation_data['save_instrumentals']): + + sf.write(self.latest_instrumental_path, + self.loop_data['wav_instrument'].T, self.loop_data['sampling_rate']) + # Vocal + if (vocal_name is not None and + self.seperation_data['save_vocals']): + sf.write(self.latest_vocal_path, + self.loop_data['wav_vocals'].T, self.loop_data['sampling_rate']) + + def _save_mask(self): + """ + Save output image + """ + mask_path = os.path.join(self.general_data['folder_path'], self.loop_data['file_base_name']) + with open('{}_Instruments.jpg'.format(mask_path), mode='wb') as f: + image = spec_utils.spectrogram_to_image(self.loop_data['y_spec']) + _, bin_image = cv2.imencode('.jpg', image) + bin_image.tofile(f) + with open('{}_Vocals.jpg'.format(mask_path), mode='wb') as f: + image = spec_utils.spectrogram_to_image(self.loop_data['v_spec']) + _, bin_image = cv2.imencode('.jpg', image) + bin_image.tofile(f) + + # -Other Methods- + def _get_progress(self, progress_step: Optional[float] = None) -> float: + """ + Get current conversion progress in percent + """ + if progress_step is not None: + self.loop_data['progress_step'] = progress_step + try: + base = (100 / self.general_data['total_files']) + progress = base * (self.loop_data['file_num'] - 1) + progress += (base / self.general_data['total_loops']) * \ + (self.loop_data['loop_num'] + self.loop_data['progress_step']) + except TypeError: + # One data point not specified yet + progress = 0 + + return progress + + +class WorkerSignals(QtCore.QObject): + ''' + Defines the signals available from a running worker thread. + + Supported signals are: + + finished + str: Time elapsed + message + str: Message to write to GUI + progress + int (0-100): Progress update + error + Tuple[str, str]: + Index 0: Error Message + Index 1: Detailed Message + + ''' + start = QtCore.Signal() + finished = QtCore.Signal(str, tuple) + message = QtCore.Signal(str) + progress = QtCore.Signal(int) + error = QtCore.Signal(tuple) + + +class VocalRemoverWorker(VocalRemover, QtCore.QRunnable): + ''' + Threaded Vocal Remover + + Only use in conjunction with GUI + ''' + + def __init__(self, logger, seperation_data: dict = {}): + super(VocalRemoverWorker, self).__init__(seperation_data, logger=logger) + super(VocalRemover, self).__init__(seperation_data, logger=logger) + super(QtCore.QRunnable, self).__init__() + self.signals = WorkerSignals() + self.logger = logger + self.seperation_data = seperation_data + self.setAutoDelete(False) + + @ QtCore.Slot() + def run(self): + """ + Seperate files + """ + stime = time.perf_counter() + + try: + self.signals.start.emit() + self.logger.info(msg='----- The seperation has started! -----') + try: + self.seperate_files() + except RuntimeError: + # Application was forcefully closed + print('Application forcefully closed') + return + except Exception as e: + self.logger.exception(msg='An Exception has occurred!') + traceback_text = ''.join(traceback.format_tb(e.__traceback__)) + message = f'Traceback Error: "{traceback_text}"\n{type(e).__name__}: "{e}"\nIf the issue is not clear, please contact the creator and attach a screenshot of the detailed message with the file and settings that caused it!' + print(traceback_text) + print(type(e).__name__, e) + self.signals.error.emit([str(e), message]) + return + elapsed_seconds = int(time.perf_counter() - stime) + elapsed_time = str(dt.timedelta(seconds=elapsed_seconds)) + self.signals.finished.emit(elapsed_time, [self.latest_instrumental_path, self.latest_vocal_path]) + + def write_to_gui(self, text: Optional[str] = None, include_base_text: bool = True, progress_step: Optional[float] = None): + if text is not None: + if include_base_text: + # Include base text + text = f"{self.loop_data['command_base_text']} {text}" + self.signals.message.emit(text) + + if progress_step is not None: + self.signals.progress.emit(self._get_progress(progress_step)) + + def _save_files(self): + """ + Also save files in temp location for in GUI audio playback + """ + super()._save_files() + if self.loop_data['loop_num'] == (self.general_data['total_loops'] - 1): # Last loop + sf.write(os.path.join(ResourcePaths.tempDir, self.latest_instrumental_path), + self.loop_data['wav_instrument'].T, self.loop_data['sampling_rate']) + sf.write(os.path.join(ResourcePaths.tempDir, self.latest_vocal_path), + self.loop_data['wav_vocals'].T, self.loop_data['sampling_rate']) diff --git a/src/resources/themes/dark.qss b/src/resources/themes/dark.qss index 1f9fbb2..dcbf2d4 100644 --- a/src/resources/themes/dark.qss +++ b/src/resources/themes/dark.qss @@ -2,6 +2,9 @@ * { font: 10pt "Yu Gothic UI"; } +*::disabled { + color: #888; +} *[title="true"], QGroupBox { font: 15pt "Yu Gothic UI"; @@ -50,11 +53,11 @@ QRadioButton[menu="true"]::indicator::hover { } QRadioButton[menu="true"]::checked, QRadioButton[menu="true"]::indicator::checked { - border-left: 5px solid qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0.505682 rgba(0, 120, 212, 255), stop:1 rgba(255, 255, 255, 0)); + border-left: 5px solid qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0.505682 #368ADD, stop:1 rgba(255, 255, 255, 0)); } /* Command clear */ QPushButton[clear="true"] { - border: 2px solid rgb(109, 213, 237); + border: 2px solid #368ADD; border-radius: 5px; color: #FFF; } @@ -71,7 +74,7 @@ QPushButton[language="true"] { border: none; } QPushButton[language="true"]:checked { - border: 3px solid rgb(109, 213, 237); + border: 3px solid #368ADD; } /* Export */ QLabel[path="true"] { @@ -83,8 +86,8 @@ QLabel[path="true"] { QPushButton[seperate="true"] { border-width: 2px; border-style: solid; - border-radius: 15px; - border-color: rgb(109, 213, 237); + border-radius: 7px; + border-color: #368ADD; background-color: rgba(109, 213, 237, 4); } QPushButton[seperate="true"]:hover { @@ -98,33 +101,37 @@ QPushButton[musicSelect="true"] { color: rgb(160, 160, 160); border-width: 3px; border-style: dotted; - border-color: rgb(160, 160, 160); + background-color: #2d2d2d; + border-color: #424242; border-radius: 5px; } -/* QPushButton[musicSelect="true"]:hover { - background-color: rgb(2, 24, 53); +QPushButton[musicSelect="true"]:hover { + background-color: #333333; } QPushButton[musicSelect="true"]:pressed { - background-color: rgb(1, 24, 61); -} */ + background-color: #404040; +} + QListWidget[musicSelect="true"] { font-size: 13pt; - background-color: rgb(12, 23, 40); - alternate-background-color: rgb(2, 18, 40); + background-color: #303030; + alternate-background-color: #424242; outline: none; + border-radius: 5px; } QListWidget[musicSelect="true"]::item { outline: none; border: none; + border-radius: 5px; } -QScrollBar[musicSelect="true"] { - background-color: none; +QListWidget[musicSelect="true"]::item:selected { + background-color: #368ADD; } /* Command Line*/ QTextBrowser { border-left: 2px; border-style: solid; - border-color: rgb(109, 213, 237); + border-color: #368ADD; font: 8pt "Courier"; } /* Audio Player */ @@ -132,7 +139,7 @@ QLabel[audioPlayer="true"] { color: rgba(160, 160, 160, 80); border-width: 3px; border-style: dotted; - border-color: rgb(60, 60, 80); + border-color: #424242; border-radius: 5px; } QPushButton[audioPlayer="true"] { @@ -145,8 +152,8 @@ QSlider[audioPlayer="true"]::groove:horizontal { } QSlider[audioPlayer="true"]::handle:horizontal { - background-color: rgb(109, 213, 237); - border: 2px solid rgb(109, 213, 237); + background-color: #368ADD; + border: 2px solid #368ADD; width: 10px; margin-top: -5px; margin-bottom: -5px; diff --git a/src/resources/themes/light.qss b/src/resources/themes/light.qss index 94a42fb..e5ca387 100644 --- a/src/resources/themes/light.qss +++ b/src/resources/themes/light.qss @@ -2,6 +2,9 @@ * { font: 10pt "Yu Gothic UI"; } +*::disabled { + color: #888; +} *[title="true"], QGroupBox { font: 15pt "Yu Gothic UI"; @@ -94,6 +97,7 @@ QPushButton[seperate="true"]:pressed { background-color: rgba(109, 213, 237, 30); } /* Music File Selection */ +/* QPushButton[musicSelect="true"] { color: rgb(160, 160, 160); border-width: 3px; @@ -119,7 +123,7 @@ QListWidget[musicSelect="true"]::item { } QScrollBar[musicSelect="true"] { background-color: none; -} +}*/ /* Command Line*/ QTextBrowser { border-left: 2px; @@ -132,7 +136,7 @@ QLabel[audioPlayer="true"] { color: rgba(160, 160, 160, 80); border-width: 3px; border-style: dotted; - border-color: rgb(60, 60, 80); + border-color: #424242; border-radius: 5px; } QPushButton[audioPlayer="true"] { diff --git a/src/resources/themes/temp.qss b/src/resources/themes/temp.qss deleted file mode 100644 index fc7caf1..0000000 --- a/src/resources/themes/temp.qss +++ /dev/null @@ -1,180 +0,0 @@ -/* --- General --- */ -* { - font: 10pt "Yu Gothic UI"; - color: rgb(255, 255, 255); - background-color: none; - background: rgb(0, 0, 0); -} -*[title="true"], -QGroupBox { - font: 15pt "Yu Gothic UI"; -} -QLineEdit, -QComboBox { - background: none; - color: #000; -} -QCheckBox { - color: #CCC; -} -QToolTip { - color: rgb(0, 0, 0); -} -QScrollBar:horizontal { - border: 2px solid green; - background: #171717; - height: 15px; - margin: 0px 40px 0 0px; -} -QScrollBar::handle:vertical { - background: #4d4d4d; - min-hieght: 20px; -} - -QComboBox QAbstractItemView { - border: 1px solid rgba(0, 120, 212, 122); - outline: none; - background-color: rgb(31, 31, 31); - selection-background-color: rgb(51, 51, 51); -} -QPushButton { - background-color: rgb(51, 121, 217); - border: none; -} -QPushButton:hover { - background-color: rgb(173, 216, 255); -} -QPushButton:pressed { - background-color: rgb(23, 66, 118); -} -QLineEdit:disabled { - color: #222; - border: 1px solid gray; - background-color: #999; -} -/* --- Settings Window Specific --- */ -/* Left Menu */ -QRadioButton[menu="true"]::indicator { - width: 0px; - height: 0px; -} -QFrame[menu="true"] { - background-color: rgb(31, 31, 31); -} -QRadioButton[menu="true"]::unchecked, -QRadioButton[menu="true"]::indicator::unchecked { - background-color: rgb(31, 31, 31); - padding: 1px; -} -QRadioButton[menu="true"]::unchecked::hover, -QRadioButton[menu="true"]::indicator::hover { - background-color: rgb(51, 51, 51); -} -QRadioButton[menu="true"]::checked, -QRadioButton[menu="true"]::indicator::checked { - border-left: 5px solid qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0.505682 rgba(0, 120, 212, 255), stop:1 rgba(255, 255, 255, 0)); -} -/* Command clear */ -QPushButton[clear="true"] { - border: 2px solid rgb(109, 213, 237); - border-radius: 5px; - color: #FFF; -} -QPushButton[clear="true"]:hover { - background-color: rgb(25, 45, 60); -} -QPushButton[clear="true"]:pressed { - background-color: rgb(49, 96, 107); -} -/* Language */ -QPushButton[language="true"] { - border-radius: 10px; - background-color: rgba(255, 255, 255, 5); - border: none; -} -QPushButton[language="true"]:checked { - border: 3px solid rgb(109, 213, 237); -} -/* Export */ -QLabel[path="true"] { - font: 7pt "Yu Gothic UI"; - color: #ccc; -} -/* --- Main Window Specific --- */ -/* Seperate Button */ -QPushButton[seperate="true"] { - border-width: 2px; - border-style: solid; - border-radius: 15px; - border-color: rgb(109, 213, 237); - background-color: rgba(109, 213, 237, 4); -} -QPushButton[seperate="true"]:hover { - background-color: rgba(109, 213, 237, 10); -} -QPushButton[seperate="true"]:pressed { - background-color: rgba(109, 213, 237, 30); -} -/* Music File Selection */ -QPushButton[musicSelect="true"] { - color: rgb(160, 160, 160); - border-width: 3px; - border-style: dotted; - border-color: rgb(160, 160, 160); - border-radius: 5px; -} -QPushButton[musicSelect="true"]:hover { - background-color: rgb(2, 24, 53); -} -QPushButton[musicSelect="true"]:pressed { - background-color: rgb(1, 24, 61); -} -QListWidget[musicSelect="true"] { - font-size: 13pt; - background-color: rgb(12, 23, 40); - alternate-background-color: rgb(2, 18, 40); - outline: none; -} -QListWidget[musicSelect="true"]::item { - outline: none; - border: none; -} -QScrollBar[musicSelect="true"] { - background-color: none; -} -/* Command Line*/ -QTextBrowser { - border-left: 2px; - border-style: solid; - border-color: rgb(109, 213, 237); - font: 8pt "Courier"; -} -/* Audio Player */ -QLabel[audioPlayer="true"] { - color: rgba(160, 160, 160, 80); - border-width: 3px; - border-style: dotted; - border-color: rgb(60, 60, 80); - border-radius: 5px; -} -QPushButton[audioPlayer="true"] { - border: none; -} -QSlider[audioPlayer="true"]::groove:horizontal { - background-color: rgb(44, 51, 65); - height: 4px; - border-radius: 2px; -} - -QSlider[audioPlayer="true"]::handle:horizontal { - background-color: rgb(109, 213, 237); - border: 2px solid rgb(109, 213, 237); - width: 10px; - margin-top: -5px; - margin-bottom: -5px; - border-radius: 5px; -} - -QSlider[audioPlayer="true"]::handle:horizontal:hover { - border-radius: 5px; -} diff --git a/src/windows/design/mainwindow_ui.py b/src/windows/design/mainwindow_ui.py index f6addfb..a769c5d 100644 --- a/src/windows/design/mainwindow_ui.py +++ b/src/windows/design/mainwindow_ui.py @@ -17,13 +17,13 @@ class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") - MainWindow.resize(911, 559) + MainWindow.resize(947, 559) MainWindow.setMinimumSize(QSize(0, 0)) MainWindow.setStyleSheet(u"") self.horizontalLayout_2 = QHBoxLayout(MainWindow) self.horizontalLayout_2.setSpacing(0) self.horizontalLayout_2.setObjectName(u"horizontalLayout_2") - self.horizontalLayout_2.setContentsMargins(0, 0, 5, 0) + self.horizontalLayout_2.setContentsMargins(0, 0, 0, 0) self.frame_5 = QFrame(MainWindow) self.frame_5.setObjectName(u"frame_5") self.frame_5.setMinimumSize(QSize(650, 0)) @@ -87,10 +87,9 @@ class Ui_MainWindow(object): self.stackedWidget_musicFiles.addWidget(self.page_select) self.page_display = QWidget() self.page_display.setObjectName(u"page_display") - self.verticalLayout_4 = QVBoxLayout(self.page_display) - self.verticalLayout_4.setSpacing(0) - self.verticalLayout_4.setObjectName(u"verticalLayout_4") - self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) + self.horizontalLayout_5 = QHBoxLayout(self.page_display) + self.horizontalLayout_5.setObjectName(u"horizontalLayout_5") + self.horizontalLayout_5.setContentsMargins(0, 0, 0, 0) self.listWidget_musicFiles = QListWidget(self.page_display) self.listWidget_musicFiles.setObjectName(u"listWidget_musicFiles") sizePolicy3 = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored) @@ -99,7 +98,6 @@ class Ui_MainWindow(object): sizePolicy3.setHeightForWidth( self.listWidget_musicFiles.sizePolicy().hasHeightForWidth()) self.listWidget_musicFiles.setSizePolicy(sizePolicy3) - self.listWidget_musicFiles.setMaximumSize(QSize(16777215, 16777215)) self.listWidget_musicFiles.setFrameShape(QFrame.NoFrame) self.listWidget_musicFiles.setLineWidth(0) self.listWidget_musicFiles.setHorizontalScrollBarPolicy( @@ -108,11 +106,39 @@ class Ui_MainWindow(object): QAbstractItemView.NoEditTriggers) self.listWidget_musicFiles.setAlternatingRowColors(True) self.listWidget_musicFiles.setSelectionMode( - QAbstractItemView.NoSelection) + QAbstractItemView.ExtendedSelection) self.listWidget_musicFiles.setWordWrap(True) self.listWidget_musicFiles.setProperty("musicSelect", True) - self.verticalLayout_4.addWidget(self.listWidget_musicFiles) + self.horizontalLayout_5.addWidget(self.listWidget_musicFiles) + + self.frame_7 = QFrame(self.page_display) + self.frame_7.setObjectName(u"frame_7") + self.frame_7.setMaximumSize(QSize(35, 16777215)) + self.frame_7.setFrameShape(QFrame.NoFrame) + self.frame_7.setFrameShadow(QFrame.Raised) + self.verticalLayout_4 = QVBoxLayout(self.frame_7) + self.verticalLayout_4.setObjectName(u"verticalLayout_4") + self.verticalLayout_4.setContentsMargins(0, 0, 0, 0) + self.pushButton_add = QPushButton(self.frame_7) + self.pushButton_add.setObjectName(u"pushButton_add") + self.pushButton_add.setMinimumSize(QSize(35, 35)) + self.pushButton_add.setMaximumSize(QSize(35, 35)) + self.pushButton_add.setCursor(QCursor(Qt.PointingHandCursor)) + self.pushButton_add.setText(u"+") + + self.verticalLayout_4.addWidget(self.pushButton_add) + + self.pushButton_delete = QPushButton(self.frame_7) + self.pushButton_delete.setObjectName(u"pushButton_delete") + self.pushButton_delete.setMinimumSize(QSize(35, 35)) + self.pushButton_delete.setMaximumSize(QSize(35, 35)) + self.pushButton_delete.setCursor(QCursor(Qt.PointingHandCursor)) + self.pushButton_delete.setText(u"-") + + self.verticalLayout_4.addWidget(self.pushButton_delete) + + self.horizontalLayout_5.addWidget(self.frame_7, 0, Qt.AlignTop) self.stackedWidget_musicFiles.addWidget(self.page_display) @@ -306,6 +332,7 @@ class Ui_MainWindow(object): self.pushButton_seperate.setObjectName(u"pushButton_seperate") self.pushButton_seperate.setMinimumSize(QSize(160, 0)) self.pushButton_seperate.setMaximumSize(QSize(16777215, 50)) + self.pushButton_seperate.setCursor(QCursor(Qt.PointingHandCursor)) self.pushButton_seperate.setStyleSheet(u"border-top-right-radius: 0px;\n" "border-bottom-right-radius: 0px;") self.pushButton_seperate.setProperty("seperate", True) @@ -317,6 +344,7 @@ class Ui_MainWindow(object): self.pushButton_settings.setObjectName(u"pushButton_settings") self.pushButton_settings.setMinimumSize(QSize(50, 50)) self.pushButton_settings.setMaximumSize(QSize(50, 50)) + self.pushButton_settings.setCursor(QCursor(Qt.PointingHandCursor)) self.pushButton_settings.setStyleSheet(u"border-left: none;\n" "border-top-left-radius: 0px;\n" "border-bottom-left-radius: 0px;") @@ -393,11 +421,10 @@ class Ui_MainWindow(object): self.horizontalLayout_2.addWidget(self.textBrowser_command) self.horizontalLayout_2.setStretch(0, 3) - self.horizontalLayout_2.setStretch(1, 2) self.retranslateUi(MainWindow) - self.stackedWidget_musicFiles.setCurrentIndex(0) + self.stackedWidget_musicFiles.setCurrentIndex(1) self.stackedWidget_vocals.setCurrentIndex(0) self.stackedWidget_instrumentals.setCurrentIndex(0) diff --git a/src/windows/mainwindow.py b/src/windows/mainwindow.py index 1334a6a..64a61f9 100644 --- a/src/windows/mainwindow.py +++ b/src/windows/mainwindow.py @@ -74,173 +74,6 @@ class MainWindow(QtWidgets.QWidget): self.instrumentals_audioPlayer: AudioPlayer self.vocals_audioPlayer: AudioPlayer self.tempAudioFilePaths: Optional[Tuple[str, str]] = None - # -Initialization methods- - - def setup_window(self): - """ - Set up the window with binds, images, saved settings - - (Only run right after window initialization of main and settings window) - """ - def load_geometry(): - """ - Load the geometry of this window - """ - # Window is centered on primary window - default_size = self.size() - default_pos = QtCore.QPoint() - default_pos.setX((self.app.primaryScreen().size().width() / 2) - default_size.width() / 2) - default_pos.setY((self.app.primaryScreen().size().height() / 2) - default_size.height() / 2) - # Get data - self.settings.beginGroup(self.__class__.__name__.lower()) - size = self.settings.value('size', - default_size) - pos = self.settings.value('pos', - default_pos) - isMaximized = self.settings.value('isMaximized', - False, - type=bool) - self.settings.endGroup() - # Apply data - self.move(pos) - if isMaximized: - self.setWindowState(Qt.WindowMaximized) - else: - self.resize(size) - - def load_images(): - """ - Load the images for this window and assign them to their widgets - """ - # Settings button - self.settings_img = QtGui.QPixmap(ResourcePaths.images.settings) - self.ui.pushButton_settings.setIcon(self.settings_img) - self.ui.pushButton_settings.setIconSize(QtCore.QSize(25, 25)) - - def bind_widgets(): - """ - Bind the widgets here - """ - # -Override binds- - # Music file drag & drop - self.ui.stackedWidget_musicFiles.dragEnterEvent = self.stackedWidget_musicFiles_dragEnterEvent - self.ui.stackedWidget_musicFiles.dropEvent = self.stackedWidget_musicFiles_dropEvent - self.ui.pushButton_musicFiles.clicked.connect(self.pushButton_musicFiles_clicked) - # -Pushbuttons- - self.ui.pushButton_settings.clicked.connect(self.pushButton_settings_clicked) - self.ui.pushButton_seperate.clicked.connect(self.pushButton_seperate_clicked) - - def create_animation_objects(): - """ - Create the animation objects that are used - multiple times here - """ - def style_progressbar(): - """ - Style pogressbar manually as when styled in Qt Designer - a bug occurs that prevents smooth animation of progressbar - """ - self.ui.progressBar.setStyleSheet("""QProgressBar:horizontal { - border: 0px solid gray; - } - QProgressBar::chunk { - background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0.0795455 rgba(33, 147, 176, 255), stop:1 rgba(109, 213, 237, 255)); - border-top-right-radius: 2px; - border-bottom-right-radius: 2px; - } """) - self.seperation_update_progress(0) - # -Progress Bar- - self.pbar_animation = QtCore.QPropertyAnimation(self.ui.progressBar, b"value", - parent=self) - # This is all to prevent the progressbar animation not working propertly - self.pbar_animation.setDuration(8) - self.pbar_animation.setStartValue(0) - self.pbar_animation.setEndValue(8) - self.pbar_animation.start() - self.pbar_animation.setDuration(500) - QtCore.QTimer.singleShot(1000, lambda: style_progressbar()) - # -Settings Icon- - - def rotate_settings_icon(): - rotation = self.settings_ani.currentValue() - t = QtGui.QTransform() - t = t.rotate(rotation) - new_pixmap = self.settings_img.transformed(t, QtCore.Qt.FastTransformation) - xoffset = (new_pixmap.width() - self.settings_img.width()) / 2 - yoffset = (new_pixmap.height() - self.settings_img.height()) / 2 - new_pixmap = new_pixmap.copy(xoffset, yoffset, self.settings_img.width(), self.settings_img.height()) - self.ui.pushButton_settings.setIcon(new_pixmap) - - self.settings_ani = QtCore.QVariantAnimation(self) - self.settings_ani.setDuration(1750) - self.settings_ani.setEasingCurve(QtCore.QEasingCurve.OutBack) - self.settings_ani.setStartValue(0.0) - self.settings_ani.setEndValue(-180.0) - self.settings_ani.valueChanged.connect(rotate_settings_icon) - - # -Before setup- - self.logger.info('Main -> Setting up', - indent_forwards=True) - # Load saved settings for widgets - self._load_data() - # Audio Players - self.instrumentals_audioPlayer = AudioPlayer(self.app, - self.ui.pushButton_play_instrumentals, - self.ui.horizontalSlider_instrumentals, - self.ui.pushButton_menu_instrumentals) - self.vocals_audioPlayer = AudioPlayer(self.app, - self.ui.pushButton_play_vocals, - self.ui.horizontalSlider_vocals, - self.ui.pushButton_menu_vocals) - # Temp func - self.tempAudioFilePaths = [os.path.join(ResourcePaths.tempDir, 'temp_instrumentals.wav'), - os.path.join(ResourcePaths.tempDir, 'temp_vocals.wav')] - self._deactivate_audio_players() - - # -Setup- - load_geometry() - load_images() - bind_widgets() - create_animation_objects() - self.show() - - # -After setup- - # Create WinTaskbar - self.winTaskbar = QWinTaskbarButton(self) - self.winTaskbar.setWindow(self.windowHandle()) - self.winTaskbar_progress = self.winTaskbar.progress() - # Create instance - self.vocalRemoverRunnable = converter_v4.VocalRemoverWorker(logger=self.logger) - # Bind events - self.vocalRemoverRunnable.signals.start.connect(self.seperation_start) - self.vocalRemoverRunnable.signals.message.connect(self.seperation_write) - self.vocalRemoverRunnable.signals.progress.connect(self.seperation_update_progress) - self.vocalRemoverRunnable.signals.error.connect(self.seperation_error) - self.vocalRemoverRunnable.signals.finished.connect(self.seperation_finish) - # Late update - self.update_window() - self.logger.indent_backwards() - - def _load_data(self, default: bool = False): - """ - Load the data for this window - - (Only run right after window initialization or to reset settings) - - Parameters: - default(bool): - Reset to the default settings - """ - self.settings.beginGroup('mainwindow') - if default: - # Delete settings group - self.settings.remove('mainwindow') - - # -Load Settings- - # None - - # -Done- - self.settings.endGroup() # -Widget Binds- def pushButton_settings_clicked(self): @@ -269,26 +102,33 @@ class MainWindow(QtWidgets.QWidget): # Start seperation self.app.threadpool.start(self.vocalRemoverRunnable) - def pushButton_musicFiles_clicked(self): + def pushButton_delete_clicked(self): """ - Open music file selection dialog + Delete selected presets after asking for + confirmation """ - self.logger.info('Selecting Music Files...', - indent_forwards=True) - paths = QtWidgets.QFileDialog.getOpenFileNames(parent=self, - caption='Select Music Files', - dir=self.inputsDirectory, - )[0] - - if not paths: - # No files specified - self.logger.info('No files selected!',) - self.logger.indent_backwards() + selected_items = self.ui.listWidget_musicFiles.selectedItems() + if not len(selected_items): return - self.inputsDirectory = os.path.dirname(paths[0]) - self.add_to_input_paths(paths) - self.logger.indent_backwards() + # Some paths already selected + msg = QtWidgets.QMessageBox() + msg.setWindowTitle(self.tr('Confirmation')) + msg.setIcon(QtWidgets.QMessageBox.Icon.Warning) + msg.setText(f'You will remove {len(selected_items)} items. Do you wish to continue?') + msg.setStandardButtons(QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No) + msg.setWindowFlag(Qt.WindowStaysOnTopHint) + val = msg.exec_() + + if val == QtWidgets.QMessageBox.No: + # Cancel + return + + removed_rows = [self.ui.listWidget_musicFiles.row(item) for item in selected_items] + self.inputPaths = [path for row, path in enumerate(self.inputPaths) if not row in removed_rows] + + # -Update settings window- + self.update_window() def stackedWidget_musicFiles_dragEnterEvent(self, event: QtGui.QDragEnterEvent): """ @@ -317,6 +157,31 @@ class MainWindow(QtWidgets.QWidget): self.add_to_input_paths(inputPaths) self.logger.indent_backwards() + def listWidget_musicFiles_itemDoubleClicked(self, item): + path = item.data(Qt.UserRole) + subprocess.Popen(r'explorer /select,"{0}"'.format(path.replace("/", "\\"))) + + def chooseMusicFiles(self): + """ + Open music file selection dialog + """ + self.logger.info('Selecting Music Files...', + indent_forwards=True) + paths = QtWidgets.QFileDialog.getOpenFileNames(parent=self, + caption='Select Music Files', + dir=self.inputsDirectory, + )[0] + + if not paths: + # No files specified + self.logger.info('No files selected!',) + self.logger.indent_backwards() + return + + self.inputsDirectory = os.path.dirname(paths[0]) + self.add_to_input_paths(paths) + self.logger.indent_backwards() + def add_to_input_paths(self, paths: list): """ Checks if paths are already selected. @@ -491,6 +356,177 @@ class MainWindow(QtWidgets.QWidget): self.instrumentals_audioPlayer.setMedia(QtMultimedia.QMediaContent()) self.vocals_audioPlayer.setMedia(QtMultimedia.QMediaContent()) + # -Initialization methods- + def setup_window(self): + """ + Set up the window with binds, images, saved settings + + (Only run right after window initialization of main and settings window) + """ + def load_geometry(): + """ + Load the geometry of this window + """ + # Window is centered on primary window + default_size = self.size() + default_pos = QtCore.QPoint() + default_pos.setX((self.app.primaryScreen().size().width() / 2) - default_size.width() / 2) + default_pos.setY((self.app.primaryScreen().size().height() / 2) - default_size.height() / 2) + # Get data + self.settings.beginGroup(self.__class__.__name__.lower()) + size = self.settings.value('size', + default_size) + pos = self.settings.value('pos', + default_pos) + isMaximized = self.settings.value('isMaximized', + False, + type=bool) + self.settings.endGroup() + # Apply data + self.move(pos) + if isMaximized: + self.setWindowState(Qt.WindowMaximized) + else: + self.resize(size) + + def load_images(): + """ + Load the images for this window and assign them to their widgets + """ + # Settings button + self.settings_img = QtGui.QPixmap(ResourcePaths.images.settings) + self.ui.pushButton_settings.setIcon(self.settings_img) + self.ui.pushButton_settings.setIconSize(QtCore.QSize(25, 25)) + + def bind_widgets(): + """ + Bind the widgets here + """ + # -Override binds- + # Music file drag & drop + self.ui.stackedWidget_musicFiles.dragEnterEvent = self.stackedWidget_musicFiles_dragEnterEvent + self.ui.stackedWidget_musicFiles.dropEvent = self.stackedWidget_musicFiles_dropEvent + # -Pushbuttons- + self.ui.pushButton_musicFiles.clicked.connect(self.chooseMusicFiles) + self.ui.pushButton_settings.clicked.connect(self.pushButton_settings_clicked) + self.ui.pushButton_seperate.clicked.connect(self.pushButton_seperate_clicked) + self.ui.pushButton_add.clicked.connect(self.chooseMusicFiles) + self.ui.pushButton_delete.clicked.connect(self.pushButton_delete_clicked) + # -ListWidget- + self.ui.listWidget_musicFiles.itemDoubleClicked.connect(self.listWidget_musicFiles_itemDoubleClicked) + + def create_animation_objects(): + """ + Create the animation objects that are used + multiple times here + """ + def style_progressbar(): + """ + Style pogressbar manually as when styled in Qt Designer + a bug occurs that prevents smooth animation of progressbar + """ + self.ui.progressBar.setStyleSheet("""QProgressBar:horizontal { + border: 0px solid gray; + } + QProgressBar::chunk { + background-color: qlineargradient(spread:pad, x1:0, y1:0, x2:1, y2:0, stop:0.0795455 #368ADD, stop:1 #2180DF); + border-top-right-radius: 2px; + border-bottom-right-radius: 2px; + } """) + self.seperation_update_progress(0) + # -Progress Bar- + self.pbar_animation = QtCore.QPropertyAnimation(self.ui.progressBar, b"value", + parent=self) + # This is all to prevent the progressbar animation not working propertly + self.pbar_animation.setDuration(8) + self.pbar_animation.setStartValue(0) + self.pbar_animation.setEndValue(8) + self.pbar_animation.start() + self.pbar_animation.setDuration(500) + QtCore.QTimer.singleShot(1000, lambda: style_progressbar()) + # -Settings Icon- + + def rotate_settings_icon(): + rotation = self.settings_ani.currentValue() + t = QtGui.QTransform() + t = t.rotate(rotation) + new_pixmap = self.settings_img.transformed(t, QtCore.Qt.FastTransformation) + xoffset = (new_pixmap.width() - self.settings_img.width()) / 2 + yoffset = (new_pixmap.height() - self.settings_img.height()) / 2 + new_pixmap = new_pixmap.copy(xoffset, yoffset, self.settings_img.width(), self.settings_img.height()) + self.ui.pushButton_settings.setIcon(new_pixmap) + + self.settings_ani = QtCore.QVariantAnimation(self) + self.settings_ani.setDuration(1750) + self.settings_ani.setEasingCurve(QtCore.QEasingCurve.OutBack) + self.settings_ani.setStartValue(0.0) + self.settings_ani.setEndValue(-180.0) + self.settings_ani.valueChanged.connect(rotate_settings_icon) + + # -Before setup- + self.logger.info('Main -> Setting up', + indent_forwards=True) + # Load saved settings for widgets + self._load_data() + # Audio Players + self.instrumentals_audioPlayer = AudioPlayer(self.app, + self.ui.pushButton_play_instrumentals, + self.ui.horizontalSlider_instrumentals, + self.ui.pushButton_menu_instrumentals) + self.vocals_audioPlayer = AudioPlayer(self.app, + self.ui.pushButton_play_vocals, + self.ui.horizontalSlider_vocals, + self.ui.pushButton_menu_vocals) + # Temp func + self.tempAudioFilePaths = [os.path.join(ResourcePaths.tempDir, 'temp_instrumentals.wav'), + os.path.join(ResourcePaths.tempDir, 'temp_vocals.wav')] + self._deactivate_audio_players() + + # -Setup- + load_geometry() + load_images() + bind_widgets() + create_animation_objects() + self.show() + + # -After setup- + # Create WinTaskbar + self.winTaskbar = QWinTaskbarButton(self) + self.winTaskbar.setWindow(self.windowHandle()) + self.winTaskbar_progress = self.winTaskbar.progress() + # Create instance + self.vocalRemoverRunnable = converter_v4.VocalRemoverWorker(logger=self.logger) + # Bind events + self.vocalRemoverRunnable.signals.start.connect(self.seperation_start) + self.vocalRemoverRunnable.signals.message.connect(self.seperation_write) + self.vocalRemoverRunnable.signals.progress.connect(self.seperation_update_progress) + self.vocalRemoverRunnable.signals.error.connect(self.seperation_error) + self.vocalRemoverRunnable.signals.finished.connect(self.seperation_finish) + # Late update + self.update_window() + self.logger.indent_backwards() + + def _load_data(self, default: bool = False): + """ + Load the data for this window + + (Only run right after window initialization or to reset settings) + + Parameters: + default(bool): + Reset to the default settings + """ + self.settings.beginGroup('mainwindow') + if default: + # Delete settings group + self.settings.remove('mainwindow') + + # -Load Settings- + # None + + # -Done- + self.settings.endGroup() + # -Other Methods- def update_window(self): """ @@ -519,9 +555,9 @@ class MainWindow(QtWidgets.QWidget): """ self.ui.listWidget_musicFiles.clear() for path in self.inputPaths: - item = QCustomListWidget() - item.setTextUp(path) + item = QCustomListWidget(full_path=path) widgetItem = QtWidgets.QListWidgetItem() + widgetItem.setData(Qt.UserRole, path) widgetItem.setSizeHint(item.sizeHint()) self.ui.listWidget_musicFiles.addItem(widgetItem) self.ui.listWidget_musicFiles.setItemWidget(widgetItem, item) @@ -567,7 +603,7 @@ class MainWindow(QtWidgets.QWidget): class QCustomListWidget(QtWidgets.QWidget): - def __init__(self, parent=None): + def __init__(self, full_path: str, parent=None): super(QCustomListWidget, self).__init__(parent) self.textUpQLabel = QtWidgets.QLabel() self.allQHBoxLayout = QtWidgets.QHBoxLayout() @@ -582,6 +618,8 @@ class QCustomListWidget(QtWidgets.QWidget): font-size: 15px; background-color: none; ''') + self.full_path = full_path + self.setTextUp(os.path.basename(self.full_path)) def setTextUp(self, text): self.textUpQLabel.setText(text) diff --git a/src/windows/presetseditorwindow.py b/src/windows/presetseditorwindow.py index 5ad2fdf..803003b 100644 --- a/src/windows/presetseditorwindow.py +++ b/src/windows/presetseditorwindow.py @@ -157,6 +157,9 @@ class PresetsEditorWindow(QtWidgets.QWidget): confirmation """ selected_items = self.ui.listWidget_presets.selectedItems() + if not len(selected_items): + return + # Some paths already selected msg = QtWidgets.QMessageBox() msg.setWindowTitle(self.tr('Confirmation')) @@ -170,7 +173,7 @@ class PresetsEditorWindow(QtWidgets.QWidget): # Cancel return - for item in self.ui.listWidget_presets.selectedItems(): + for item in selected_items: row = self.ui.listWidget_presets.row(item) self.ui.listWidget_presets.takeItem(row) @@ -265,7 +268,7 @@ class PresetsEditorWindow(QtWidgets.QWidget): presets = {} for idx in range(self.ui.listWidget_presets.count()): item = self.ui.listWidget_presets.item(idx) - presets[item.text()] = item.data(Qt.UserRole) + presets[item.text()] = item.data(Qt.UserRole).copy() return presets @@ -277,6 +280,7 @@ class PresetsEditorWindow(QtWidgets.QWidget): if name in presets: return presets[name] else: + self.logger.warning(f'No preset with name: "{name}" Available preset names: "{presets.keys()}"') return {} # -Overriden methods- diff --git a/src/windows/settingswindow.py b/src/windows/settingswindow.py index dba420a..4dc86df 100644 --- a/src/windows/settingswindow.py +++ b/src/windows/settingswindow.py @@ -41,7 +41,6 @@ class SettingsWindow(QtWidgets.QWidget): self.setWindowIcon(QtGui.QIcon(ResourcePaths.images.settings)) # -Other Variables- - self.suppress_settings_change_event = False self.menu_update_methods = { 0: self.update_page_seperationSettings, 1: self.update_page_shortcuts, @@ -52,6 +51,7 @@ class SettingsWindow(QtWidgets.QWidget): self.exportDirectory = self.settings.value('user/exportDirectory', const.DEFAULT_SETTINGS['exportDirectory'], type=str) + self.search_for_preset = True # -Widget Binds- def pushButton_clearCommand_clicked(self): @@ -125,12 +125,14 @@ class SettingsWindow(QtWidgets.QWidget): """ Changed preset """ + self.search_for_preset = False name = self.ui.comboBox_presets.currentText() settings = self.app.windows['presetsEditor'].get_settings(name) for json_key in list(settings.keys()): widget_objectName = const.JSON_TO_NAME[json_key] settings[widget_objectName] = settings.pop(json_key) self.settingsManager.set_settings(settings) + self.search_for_preset = True def frame_export_dragEnterEvent(self, event: QtGui.QDragEnterEvent): """ @@ -187,11 +189,22 @@ class SettingsWindow(QtWidgets.QWidget): self.logger.indent_backwards() def settings_changed(self): - if (self.ui.comboBox_presets.currentText() and - not self.suppress_settings_change_event): - self.ui.comboBox_presets.setCurrentText('') + if self.search_for_preset: + current_settings = self.settingsManager.get_settings(0) + presets: dict = self.app.windows['presetsEditor'].get_presets() + + for preset_name, settings in presets.items(): + for json_key, value in settings.items(): + if (current_settings[const.JSON_TO_NAME[json_key]] != value): + break + else: + self.ui.comboBox_presets.setCurrentText(preset_name) + break + else: + self.ui.comboBox_presets.setCurrentIndex(0) # -Window Setup Methods- + def setup_window(self): """ Set up the window with binds, images, saved settings @@ -272,7 +285,7 @@ class SettingsWindow(QtWidgets.QWidget): if isinstance(widget, QtWidgets.QCheckBox): widget.stateChanged.connect(self.settings_changed) elif isinstance(widget, QtWidgets.QComboBox): - widget.currentIndexChanged.connect(self.settings_changed) + widget.currentTextChanged.connect(self.settings_changed) elif isinstance(widget, QtWidgets.QLineEdit): widget.textChanged.connect(self.settings_changed) elif (isinstance(widget, QtWidgets.QDoubleSpinBox) or @@ -323,6 +336,7 @@ class SettingsWindow(QtWidgets.QWidget): """ # -Before setup- + self.search_for_preset = False # Load saved settings for widgets self.settingsManager.load_window() # Update available model lists @@ -357,6 +371,8 @@ class SettingsWindow(QtWidgets.QWidget): # Load menu (Preferences) self.update_window() self.menu_loadPage(0, True) + self.search_for_preset = True + self.settings_changed() self.logger.indent_backwards() def load_window(self): @@ -400,7 +416,7 @@ class SettingsWindow(QtWidgets.QWidget): self.ui.comboBox_presets.blockSignals(True) last_text = self.ui.comboBox_presets.currentText() self.ui.comboBox_presets.clear() - self.ui.comboBox_presets.addItem('') + self.ui.comboBox_presets.addItem('Custom') for idx in range(self.app.windows['presetsEditor'].ui.listWidget_presets.count()): # Loop through every preset in the list on the window # Get item by index @@ -412,6 +428,7 @@ class SettingsWindow(QtWidgets.QWidget): if text == last_text: self.ui.comboBox_presets.setCurrentText(text) self.ui.comboBox_presets.blockSignals(False) + self.settings_changed() self.logger.indent_backwards() @@ -647,7 +664,7 @@ class SettingsManager: self.save_widgets[2] = customization_widgets self.save_widgets[3] = preferences_widgets - def get_settings(self, page_idx: Optional[int] = None) -> Dict[str, Union[bool, str]]: + def get_settings(self, page_idx: Optional[int] = None) -> Dict[str, Union[bool, str, float]]: """Obtain states of the widgets Args: @@ -665,7 +682,7 @@ class SettingsManager: TypeError: Invalid widget type in the widgets (has to be either: QCheckBox, QRadioButton, QLineEdit or QComboBox) Returns: - Dict[str, Union[bool, str]]: Widget states + Dict[str, Union[bool, str, float]]: Widget states Key - Widget object name Value - State of the widget """ @@ -693,7 +710,7 @@ class SettingsManager: return settings - def set_settings(self, settings: Dict[str, Union[bool, str]]): + def set_settings(self, settings: Dict[str, Union[bool, str, float]]): """Update states of the widgets The given dict's key should be the widgets object name @@ -714,12 +731,11 @@ class SettingsManager: Args: - settings (Dict[str, Union[bool, str]]): States of the widgets to update + settings (Dict[str, Union[bool, str, float]]): States of the widgets to update Raises: TypeError: Invalid widget type in the widgets (has to be either: QCheckBox, QRadioButton, QLineEdit or QComboBox) """ - self.win.suppress_settings_change_event = True for widget_objectName, value in settings.items(): # Get widget widget = self.win.findChild(QtCore.QObject, widget_objectName) @@ -746,7 +762,6 @@ class SettingsManager: widget.setValue(value) else: raise TypeError('Invalid widget type that is not supported!\nWidget: ', widget) - self.win.suppress_settings_change_event = False self.win.update_window() def load_window(self): @@ -757,7 +772,6 @@ class SettingsManager: """ # Before self.win.logger.info('Settings: Loading window') - # -Load states- self.win.settings.beginGroup('settingswindow') for widget in self.get_widgets(): @@ -803,6 +817,7 @@ class SettingsManager: widget.setValue(value) else: raise TypeError('Invalid widget type that is not supported!\nWidget: ', widget) + self.win.settings.endGroup() def save_window(self): diff --git a/ui_files/mainwindow.ui b/ui_files/mainwindow.ui index 9a1807d..d332baa 100644 --- a/ui_files/mainwindow.ui +++ b/ui_files/mainwindow.ui @@ -6,7 +6,7 @@ 0 0 - 911 + 947 559 @@ -22,7 +22,7 @@ - + 0 @@ -33,7 +33,7 @@ 0 - 5 + 0 0 @@ -137,7 +137,7 @@ 1 - 0 + 1 @@ -181,10 +181,7 @@ - - - 0 - + 0 @@ -205,12 +202,6 @@ 0 - - - 16777215 - 16777215 - - QFrame::NoFrame @@ -227,7 +218,7 @@ true - QAbstractItemView::NoSelection + QAbstractItemView::ExtendedSelection true @@ -237,6 +228,80 @@ + + + + + 35 + 16777215 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 35 + 35 + + + + + 35 + 35 + + + + PointingHandCursor + + + + + + + + + + + + 35 + 35 + + + + + 35 + 35 + + + + PointingHandCursor + + + - + + + + + + @@ -662,6 +727,9 @@ 50 + + PointingHandCursor + border-top-right-radius: 0px; border-bottom-right-radius: 0px; @@ -691,6 +759,9 @@ border-bottom-right-radius: 0px; 50 + + PointingHandCursor + border-left: none; border-top-left-radius: 0px; diff --git a/ui_files/settingswindow.ui b/ui_files/settingswindow.ui index 25d5be6..4518eb9 100644 --- a/ui_files/settingswindow.ui +++ b/ui_files/settingswindow.ui @@ -962,8 +962,8 @@ 0 0 - 541 - 510 + 53 + 35 @@ -1039,7 +1039,7 @@ 0 0 - 541 + 630 510 @@ -1164,7 +1164,7 @@ 0 0 - 600 + 630 510