atonatui/settings_mixin.py
2026-08-09 00:10:59 -04:00

204 lines
7 KiB
Python

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))