import os import json from functools import cached_property from dataclasses import dataclass, fields from constants import RESET, REVERSE, SOUNDFONTS _INSTRUMENT_LIST = [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(SOUNDFONTS.keys())] _SETTINGS_SAVE_FILE = 'assets/settings.json' _MATRIX_SAVE_DIR = os.path.join(os.path.dirname(__file__), "assets/saves") def get_saved_matrix_list(): if not os.path.exists(_MATRIX_SAVE_DIR): return [" [No saved matrices found]"] save_files = [ f for f in os.listdir(_MATRIX_SAVE_DIR) if os.path.isfile(os.path.join(_MATRIX_SAVE_DIR, f)) and f.endswith(".sav") ] save_files.sort() if not save_files: return [" [No saved matrices found]"] return [f"[{str(idx).rjust(2)}] {name}" for idx, name in enumerate(save_files)] _SAVED_MATRIX_LIST = get_saved_matrix_list() @dataclass class Settings: BPM: int = 140 INSTRUMENT: str = "Bassoon" OCTAVE: int = 3 OCTAVE_RANDOMIZATION: int = 0 RHYTHM_COMPLEXITY: int = 0 DEFAULT_DIVISION: int = 2 REST_PROBABILITY: int = 0 SAVE_SETTINGS: None = None SAVE_MATRIX: None = None LOAD_MATRIX: None = None @classmethod def max_len(cls): return max(len(f.name) for f in fields(cls)) + 2 @dataclass class SettingsHelp: BPM: str = "Enter a BPM between 30 - 300" INSTRUMENT: str = f"Select Soundfont:\n\n{'\n'.join(_INSTRUMENT_LIST)}" OCTAVE: str = "Change the octave. Select an octave between 1 and 5" OCTAVE_RANDOMIZATION: int = 0 RHYTHM_COMPLEXITY: str = "\n[0] Off, All Eigth Notes\n[1] On, Engage Rhythm Matrix\n[2] On, Rhythm Matrix and Rest Randomization\n[3] On, Chaos Mode, A random duration is chosen between 0.05 seconds and 1 second" DEFAULT_DIVISION: str = "\n[0] Whole Note\n[1] Half Note\n[2] Quarter Note\n[3] Eight Note" REST_PROBABILITY: str = "Probability in which a note in row will be replaced by a rest.\nEnter a value between 0 and 100, where 0 is absolutely no chance of a rest and 100 is all rests.\nThis only works in RHYTHM_COMPLEXITY MODE 2." SAVE_SETTINGS: str = "Save current settings to file" SAVE_MATRIX: str = "Save current matrix to file, Enter name of matrix to save" LOAD_MATRIX: str = f"Select Saved Matrix to Load:\n\n{'\n'.join(_SAVED_MATRIX_LIST)}" EXIT: None = None SETTINGS_RULES = { "BPM": {"type": int, "min": 30, "max": 300}, "OCTAVE": {"type": int, "min": 1, "max": 5}, "OCTAVE_RANDOMIZATION": {"type": int, "min": 0, "max": 4}, "RHYTHM_COMPLEXITY": {"type": int, "min": 0, "max": 3}, "DEFAULT_DIVISION": {"type": int, "min": 0, "max": 3}, "REST_PROBABILITY": {"type": int, "min": 0, "max": 100}, "INSTRUMENT": {"type": int, "min": 0, "max": len(SOUNDFONTS)}, "SAVE_MATRIX": {"type": str, "min": 0, "max": 10}, "LOAD_MATRIX": {"type": str, "min": 0, "max": 100}, } class SettingsMixin: @cached_property def settings(self): return [f.name for f in fields(Settings)] def save_settings(self): setting_obj = {} for field in fields(Settings): setting_obj[field.name] = getattr(Settings, field.name) with open(_SETTINGS_SAVE_FILE, "w+") as f: json.dump(setting_obj, f) def load_settings(self): with open(_SETTINGS_SAVE_FILE, "r") as f: data = json.load(f) for k, v in data.items(): setattr(Settings, k, int(v) if str(v).isdigit() else v) self.update_sf(SOUNDFONTS[Settings.INSTRUMENT]) self.bpm = int(Settings.BPM) def display_settings(self): self.clear_screen() print(f"{REVERSE} SETTINGS: {RESET}\n") for idx, setting in enumerate(self.settings): value = getattr(Settings, setting) print(f"[{idx}] {setting.ljust(Settings.max_len())}{": " + str(value) if value != None else ''}") self.user_input() self.display_settings() def user_input(self, setting_name=None): request = self.request('Enter ID of setting you want to change, "q" to exit') if request[0] == "q": self.loop() request = self.validate_request(request) setting_name = self.settings[request] if setting_name == "SAVE_SETTINGS": self.save_settings() self.message("Settings Saved..", self.display_settings, type="SUCCESS") self.display_setting(setting_name) value = self.request("Update setting by entering value") value = self.validate_new_setting(setting_name, value) setattr(Settings, setting_name, value) if setting_name == "LOAD_MATRIX": self.handle_load_matrix() if setting_name == "SAVE_MATRIX": self.handle_save_matrix() if setting_name == "BPM": self.bpm = int(Settings.BPM) if setting_name == "INSTRUMENT": instrument = list(SOUNDFONTS.keys())[value] setattr(Settings, setting_name, instrument) self.update_sf(SOUNDFONTS[instrument]) if setting_name == "OCTAVE_RANDOMIZATION": del self.octave_row del self.octave_map _ = self.octave_row _ = self.octave_map if setting_name == "OCTAVE": del self.octave_row del self.octave_map _ = self.octave_row _ = self.octave_map def validate_request(self, request): try: request = int(request) if request >= len(self.settings): raise ValueError return request except ValueError: self.message(f"'{request}' is not a valid input, try again!", self.display_settings) def display_setting(self, setting_name): self.clear_screen() print(f"{REVERSE}{setting_name.ljust(Settings.max_len())} CURRENT VALUE: {getattr(Settings, setting_name)} {RESET}\n") print(f"INFO: {getattr(SettingsHelp, setting_name)}\n") def validate_new_setting(self, setting_name, value): try: if SETTINGS_RULES[setting_name]['type'] == int: value = int(value) if value >= SETTINGS_RULES[setting_name]['min'] and value <= SETTINGS_RULES[setting_name]['max']: pass else: raise ValueError return value else: return value except ValueError: self.message(f"'{value}' is not a valid input, try again!", self.display_settings) def handle_load_matrix(self): file_name = " ".join(_SAVED_MATRIX_LIST[int(Settings.LOAD_MATRIX)].split("] ")[1:]) with open(f"assets/saves/{file_name}", 'r') as f: seed = f.readline() self.current_seed = int(seed) self.regenerate() self.use_current_seed = True def handle_save_matrix(self): Settings.SAVE_MATRIX = Settings.SAVE_MATRIX.strip().replace(".", " ").replace(" ", "_") with open(f"assets/saves/{Settings.SAVE_MATRIX}.sav", 'w+') as f: f.write(str(self.current_seed))