You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

335 lines
11 KiB

"""
Configuration Module
Manages application configuration, settings persistence, and defaults.
"""
import json
import os
from typing import Dict, Any, Optional
class Configuration:
"""
Application configuration manager.
Handles loading, saving, and managing application settings.
"""
DEFAULT_CONFIG = {
"version": "1.0",
"window": {
"width": 1200,
"height": 800,
"maximize": False,
"remember_position": True,
"x": 100,
"y": 100
},
"audio": {
"sample_rate": 22050,
"buffer_size": 512,
"master_volume": 0.8,
"enabled": True
},
"midi": {
"default_output": "",
"default_mode": "simulator",
"auto_connect": True
},
"arpeggiator": {
"default_root_note": 60, # Middle C
"default_scale": "major",
"default_pattern": "up",
"default_tempo": 120.0,
"default_octave_range": 1,
"default_note_speed": "1/8",
"default_gate": 1.0,
"default_swing": 0.0,
"default_velocity": 80
},
"channels": {
"default_synth_count": 8,
"default_instrument": 0, # Piano
"max_voices_per_synth": 3
},
"volume_patterns": {
"default_pattern": "static",
"default_speed": 1.0,
"default_intensity": 1.0,
"default_min_volume": 0.2,
"default_max_volume": 1.0,
"default_min_velocity": 40,
"default_max_velocity": 127
},
"simulator": {
"default_brightness": 1.0,
"lighting_enabled": True,
"animation_speed": 16 # ms between updates
},
"maschine": {
"enabled": False,
"device_name": "",
"auto_connect": True,
"pad_sensitivity": 1.0,
"encoder_sensitivity": 1.0
},
"presets": {
"auto_save_current": True,
"remember_last_preset": True,
"last_preset": ""
},
"interface": {
"theme": "dark",
"font_size": 10,
"show_tooltips": True,
"confirm_destructive_actions": True
}
}
def __init__(self, config_file: str = "config.json"):
self.config_file = config_file
self.config = self.DEFAULT_CONFIG.copy()
self.load_config()
def load_config(self) -> bool:
"""Load configuration from file"""
try:
if os.path.exists(self.config_file):
with open(self.config_file, 'r') as f:
loaded_config = json.load(f)
# Merge with defaults (preserves new default settings)
self._merge_config(self.config, loaded_config)
return True
except Exception as e:
print(f"Error loading configuration: {e}")
print("Using default configuration")
return False
def save_config(self) -> bool:
"""Save current configuration to file"""
try:
with open(self.config_file, 'w') as f:
json.dump(self.config, f, indent=2)
return True
except Exception as e:
print(f"Error saving configuration: {e}")
return False
def _merge_config(self, base: dict, update: dict):
"""Recursively merge configuration dictionaries"""
for key, value in update.items():
if key in base and isinstance(base[key], dict) and isinstance(value, dict):
self._merge_config(base[key], value)
else:
base[key] = value
def get(self, path: str, default: Any = None) -> Any:
"""
Get configuration value using dot notation.
Example: get("window.width") returns config["window"]["width"]
"""
keys = path.split('.')
value = self.config
try:
for key in keys:
value = value[key]
return value
except (KeyError, TypeError):
return default
def set(self, path: str, value: Any) -> bool:
"""
Set configuration value using dot notation.
Example: set("window.width", 1024)
"""
keys = path.split('.')
config_ref = self.config
try:
# Navigate to parent dictionary
for key in keys[:-1]:
if key not in config_ref:
config_ref[key] = {}
config_ref = config_ref[key]
# Set the value
config_ref[keys[-1]] = value
return True
except Exception as e:
print(f"Error setting config value {path}: {e}")
return False
def get_section(self, section: str) -> Dict[str, Any]:
"""Get entire configuration section"""
return self.config.get(section, {})
def set_section(self, section: str, values: Dict[str, Any]):
"""Set entire configuration section"""
if section not in self.config:
self.config[section] = {}
self.config[section].update(values)
def reset_to_defaults(self, section: Optional[str] = None):
"""Reset configuration to defaults"""
if section:
if section in self.DEFAULT_CONFIG:
self.config[section] = self.DEFAULT_CONFIG[section].copy()
else:
self.config = self.DEFAULT_CONFIG.copy()
def get_window_settings(self) -> Dict[str, Any]:
"""Get window-related settings"""
return self.get_section("window")
def save_window_settings(self, width: int, height: int, x: int, y: int, maximized: bool = False):
"""Save window position and size"""
window_settings = {
"width": width,
"height": height,
"x": x,
"y": y,
"maximize": maximized
}
self.set_section("window", window_settings)
self.save_config()
def get_audio_settings(self) -> Dict[str, Any]:
"""Get audio-related settings"""
return self.get_section("audio")
def get_midi_settings(self) -> Dict[str, Any]:
"""Get MIDI-related settings"""
return self.get_section("midi")
def get_arpeggiator_defaults(self) -> Dict[str, Any]:
"""Get arpeggiator default settings"""
return self.get_section("arpeggiator")
def get_channel_defaults(self) -> Dict[str, Any]:
"""Get channel default settings"""
return self.get_section("channels")
def get_volume_pattern_defaults(self) -> Dict[str, Any]:
"""Get volume pattern default settings"""
return self.get_section("volume_patterns")
def get_simulator_settings(self) -> Dict[str, Any]:
"""Get simulator settings"""
return self.get_section("simulator")
def get_maschine_settings(self) -> Dict[str, Any]:
"""Get Maschine integration settings"""
return self.get_section("maschine")
def get_preset_settings(self) -> Dict[str, Any]:
"""Get preset management settings"""
return self.get_section("presets")
def get_interface_settings(self) -> Dict[str, Any]:
"""Get interface settings"""
return self.get_section("interface")
def should_auto_save_preset(self) -> bool:
"""Check if presets should be auto-saved"""
return self.get("presets.auto_save_current", True)
def should_remember_last_preset(self) -> bool:
"""Check if last preset should be remembered"""
return self.get("presets.remember_last_preset", True)
def get_last_preset(self) -> str:
"""Get name of last used preset"""
return self.get("presets.last_preset", "")
def set_last_preset(self, preset_name: str):
"""Set name of last used preset"""
self.set("presets.last_preset", preset_name)
self.save_config()
def should_confirm_destructive_actions(self) -> bool:
"""Check if destructive actions should be confirmed"""
return self.get("interface.confirm_destructive_actions", True)
def get_theme(self) -> str:
"""Get current theme"""
return self.get("interface.theme", "dark")
def export_config(self, file_path: str) -> bool:
"""Export configuration to a different file"""
try:
with open(file_path, 'w') as f:
json.dump(self.config, f, indent=2)
return True
except Exception as e:
print(f"Error exporting configuration: {e}")
return False
def import_config(self, file_path: str) -> bool:
"""Import configuration from a file"""
try:
if os.path.exists(file_path):
with open(file_path, 'r') as f:
imported_config = json.load(f)
# Validate and merge
self._merge_config(self.config, imported_config)
self.save_config()
return True
except Exception as e:
print(f"Error importing configuration: {e}")
return False
def validate_config(self) -> bool:
"""Validate configuration values"""
valid = True
# Validate window settings
window = self.get_section("window")
if window.get("width", 0) < 800:
self.set("window.width", 1200)
valid = False
if window.get("height", 0) < 600:
self.set("window.height", 800)
valid = False
# Validate audio settings
audio = self.get_section("audio")
if audio.get("sample_rate", 0) not in [22050, 44100, 48000]:
self.set("audio.sample_rate", 22050)
valid = False
if not (0.0 <= audio.get("master_volume", 1.0) <= 1.0):
self.set("audio.master_volume", 0.8)
valid = False
# Validate arpeggiator settings
arp = self.get_section("arpeggiator")
if not (0 <= arp.get("default_root_note", 60) <= 127):
self.set("arpeggiator.default_root_note", 60)
valid = False
if not (40 <= arp.get("default_tempo", 120) <= 200):
self.set("arpeggiator.default_tempo", 120.0)
valid = False
# Validate channel settings
channels = self.get_section("channels")
if not (1 <= channels.get("default_synth_count", 8) <= 16):
self.set("channels.default_synth_count", 8)
valid = False
return valid
def get_config_info(self) -> Dict[str, Any]:
"""Get configuration metadata"""
return {
"version": self.config.get("version", "Unknown"),
"file_path": self.config_file,
"file_exists": os.path.exists(self.config_file),
"file_size": os.path.getsize(self.config_file) if os.path.exists(self.config_file) else 0,
"sections": list(self.config.keys())
}