""" 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()) }