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.
510 lines
20 KiB
510 lines
20 KiB
"""
|
|
Preset Controls GUI
|
|
|
|
Interface for saving, loading, and managing presets.
|
|
"""
|
|
|
|
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
|
|
QGroupBox, QListWidget, QPushButton, QLineEdit,
|
|
QLabel, QFileDialog, QMessageBox, QListWidgetItem,
|
|
QInputDialog, QFrame)
|
|
from PyQt5.QtCore import Qt, pyqtSlot
|
|
import json
|
|
import os
|
|
|
|
class PresetControls(QWidget):
|
|
"""Control panel for preset management"""
|
|
|
|
def __init__(self, arpeggiator, channel_manager, volume_engine):
|
|
super().__init__()
|
|
self.arpeggiator = arpeggiator
|
|
self.channel_manager = channel_manager
|
|
self.volume_engine = volume_engine
|
|
|
|
# Preset storage
|
|
self.presets = {}
|
|
self.current_preset = None
|
|
self.presets_directory = "presets"
|
|
|
|
# Ensure presets directory exists
|
|
os.makedirs(self.presets_directory, exist_ok=True)
|
|
|
|
self.setup_ui()
|
|
self.load_presets_from_directory()
|
|
|
|
def setup_ui(self):
|
|
"""Set up the user interface"""
|
|
layout = QVBoxLayout(self)
|
|
|
|
# Preset list
|
|
preset_group = self.create_preset_list()
|
|
layout.addWidget(preset_group)
|
|
|
|
# Preset operations
|
|
operations_group = self.create_operations()
|
|
layout.addWidget(operations_group)
|
|
|
|
# File operations
|
|
file_group = self.create_file_operations()
|
|
layout.addWidget(file_group)
|
|
|
|
def create_preset_list(self) -> QGroupBox:
|
|
"""Create preset list display"""
|
|
group = QGroupBox("Presets")
|
|
layout = QVBoxLayout(group)
|
|
|
|
self.preset_list = QListWidget()
|
|
self.preset_list.setMaximumHeight(200)
|
|
self.preset_list.itemClicked.connect(self.on_preset_selected)
|
|
self.preset_list.itemDoubleClicked.connect(self.on_preset_double_clicked)
|
|
layout.addWidget(self.preset_list)
|
|
|
|
# Current preset indicator
|
|
current_layout = QHBoxLayout()
|
|
current_layout.addWidget(QLabel("Current:"))
|
|
self.current_preset_label = QLabel("None")
|
|
self.current_preset_label.setStyleSheet("font-weight: bold; color: #00aa00;")
|
|
current_layout.addWidget(self.current_preset_label)
|
|
current_layout.addStretch()
|
|
layout.addLayout(current_layout)
|
|
|
|
return group
|
|
|
|
def create_operations(self) -> QGroupBox:
|
|
"""Create preset operation buttons"""
|
|
group = QGroupBox("Operations")
|
|
layout = QGridLayout(group)
|
|
|
|
# Load preset
|
|
self.load_button = QPushButton("Load Preset")
|
|
self.load_button.setEnabled(False)
|
|
self.load_button.clicked.connect(self.load_selected_preset)
|
|
layout.addWidget(self.load_button, 0, 0)
|
|
|
|
# Save current as new preset
|
|
self.save_new_button = QPushButton("Save as New...")
|
|
self.save_new_button.clicked.connect(self.save_new_preset)
|
|
layout.addWidget(self.save_new_button, 0, 1)
|
|
|
|
# Update selected preset
|
|
self.update_button = QPushButton("Update Selected")
|
|
self.update_button.setEnabled(False)
|
|
self.update_button.clicked.connect(self.update_selected_preset)
|
|
layout.addWidget(self.update_button, 1, 0)
|
|
|
|
# Delete preset
|
|
self.delete_button = QPushButton("Delete Selected")
|
|
self.delete_button.setEnabled(False)
|
|
self.delete_button.clicked.connect(self.delete_selected_preset)
|
|
self.delete_button.setStyleSheet("color: #aa6666;")
|
|
layout.addWidget(self.delete_button, 1, 1)
|
|
|
|
# Rename preset
|
|
self.rename_button = QPushButton("Rename Selected")
|
|
self.rename_button.setEnabled(False)
|
|
self.rename_button.clicked.connect(self.rename_selected_preset)
|
|
layout.addWidget(self.rename_button, 2, 0)
|
|
|
|
# Duplicate preset
|
|
self.duplicate_button = QPushButton("Duplicate Selected")
|
|
self.duplicate_button.setEnabled(False)
|
|
self.duplicate_button.clicked.connect(self.duplicate_selected_preset)
|
|
layout.addWidget(self.duplicate_button, 2, 1)
|
|
|
|
return group
|
|
|
|
def create_file_operations(self) -> QGroupBox:
|
|
"""Create file operation buttons"""
|
|
group = QGroupBox("File Operations")
|
|
layout = QHBoxLayout(group)
|
|
|
|
# Import preset
|
|
self.import_button = QPushButton("Import Preset...")
|
|
self.import_button.clicked.connect(self.import_preset)
|
|
layout.addWidget(self.import_button)
|
|
|
|
# Export preset
|
|
self.export_button = QPushButton("Export Selected...")
|
|
self.export_button.setEnabled(False)
|
|
self.export_button.clicked.connect(self.export_selected_preset)
|
|
layout.addWidget(self.export_button)
|
|
|
|
return group
|
|
|
|
def capture_current_settings(self) -> dict:
|
|
"""Capture current settings into a preset dictionary"""
|
|
preset = {
|
|
"version": "1.0",
|
|
"timestamp": None, # Will be set when saving
|
|
|
|
# Arpeggiator settings
|
|
"arpeggiator": {
|
|
"root_note": self.arpeggiator.root_note,
|
|
"scale": self.arpeggiator.scale,
|
|
"pattern_type": self.arpeggiator.pattern_type,
|
|
"octave_range": self.arpeggiator.octave_range,
|
|
"note_speed": self.arpeggiator.note_speed,
|
|
"gate": self.arpeggiator.gate,
|
|
"swing": self.arpeggiator.swing,
|
|
"velocity": self.arpeggiator.velocity,
|
|
"tempo": self.arpeggiator.tempo
|
|
},
|
|
|
|
# Channel settings
|
|
"channels": {
|
|
"active_synth_count": self.channel_manager.active_synth_count,
|
|
"channel_instruments": self.channel_manager.channel_instruments.copy()
|
|
},
|
|
|
|
# Volume pattern settings
|
|
"volume_patterns": {
|
|
"current_pattern": self.volume_engine.current_pattern,
|
|
"pattern_speed": self.volume_engine.pattern_speed,
|
|
"pattern_intensity": self.volume_engine.pattern_intensity,
|
|
"global_volume_range": self.volume_engine.global_volume_range,
|
|
"global_velocity_range": self.volume_engine.global_velocity_range,
|
|
"channel_volume_ranges": self.volume_engine.channel_volume_ranges.copy(),
|
|
"velocity_ranges": self.volume_engine.velocity_ranges.copy()
|
|
}
|
|
}
|
|
|
|
return preset
|
|
|
|
def apply_preset_settings(self, preset: dict):
|
|
"""Apply preset settings to the system"""
|
|
try:
|
|
# Apply arpeggiator settings
|
|
arp_settings = preset.get("arpeggiator", {})
|
|
self.arpeggiator.set_root_note(arp_settings.get("root_note", 60))
|
|
self.arpeggiator.set_scale(arp_settings.get("scale", "major"))
|
|
self.arpeggiator.set_pattern_type(arp_settings.get("pattern_type", "up"))
|
|
self.arpeggiator.set_octave_range(arp_settings.get("octave_range", 1))
|
|
self.arpeggiator.set_note_speed(arp_settings.get("note_speed", "1/8"))
|
|
self.arpeggiator.set_gate(arp_settings.get("gate", 1.0))
|
|
self.arpeggiator.set_swing(arp_settings.get("swing", 0.0))
|
|
self.arpeggiator.set_velocity(arp_settings.get("velocity", 80))
|
|
self.arpeggiator.set_tempo(arp_settings.get("tempo", 120.0))
|
|
|
|
# Apply channel settings
|
|
channel_settings = preset.get("channels", {})
|
|
self.channel_manager.set_active_synth_count(
|
|
channel_settings.get("active_synth_count", 8)
|
|
)
|
|
|
|
# Apply instruments
|
|
instruments = channel_settings.get("channel_instruments", {})
|
|
for channel_str, program in instruments.items():
|
|
channel = int(channel_str)
|
|
self.channel_manager.set_channel_instrument(channel, program)
|
|
|
|
# Apply volume pattern settings
|
|
volume_settings = preset.get("volume_patterns", {})
|
|
self.volume_engine.set_pattern(volume_settings.get("current_pattern", "static"))
|
|
self.volume_engine.set_pattern_speed(volume_settings.get("pattern_speed", 1.0))
|
|
self.volume_engine.set_pattern_intensity(volume_settings.get("pattern_intensity", 1.0))
|
|
|
|
# Apply global ranges
|
|
global_vol = volume_settings.get("global_volume_range", (0.2, 1.0))
|
|
global_vel = volume_settings.get("global_velocity_range", (40, 127))
|
|
self.volume_engine.set_global_ranges(
|
|
global_vol[0], global_vol[1], global_vel[0], global_vel[1]
|
|
)
|
|
|
|
# Apply individual channel ranges
|
|
ch_vol_ranges = volume_settings.get("channel_volume_ranges", {})
|
|
for channel_str, range_tuple in ch_vol_ranges.items():
|
|
channel = int(channel_str)
|
|
self.volume_engine.set_channel_volume_range(channel, range_tuple[0], range_tuple[1])
|
|
|
|
vel_ranges = volume_settings.get("velocity_ranges", {})
|
|
for channel_str, range_tuple in vel_ranges.items():
|
|
channel = int(channel_str)
|
|
self.volume_engine.set_velocity_range(channel, range_tuple[0], range_tuple[1])
|
|
|
|
except Exception as e:
|
|
QMessageBox.warning(self, "Preset Error", f"Error applying preset: {str(e)}")
|
|
|
|
@pyqtSlot(QListWidgetItem)
|
|
def on_preset_selected(self, item):
|
|
"""Handle preset selection"""
|
|
preset_name = item.text()
|
|
|
|
# Enable/disable buttons based on selection
|
|
has_selection = preset_name is not None
|
|
self.load_button.setEnabled(has_selection)
|
|
self.update_button.setEnabled(has_selection)
|
|
self.delete_button.setEnabled(has_selection)
|
|
self.rename_button.setEnabled(has_selection)
|
|
self.duplicate_button.setEnabled(has_selection)
|
|
self.export_button.setEnabled(has_selection)
|
|
|
|
@pyqtSlot(QListWidgetItem)
|
|
def on_preset_double_clicked(self, item):
|
|
"""Handle preset double-click (load preset)"""
|
|
self.load_selected_preset()
|
|
|
|
@pyqtSlot()
|
|
def load_selected_preset(self):
|
|
"""Load the selected preset"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
preset_name = current_item.text()
|
|
if preset_name in self.presets:
|
|
self.apply_preset_settings(self.presets[preset_name])
|
|
self.current_preset = preset_name
|
|
self.current_preset_label.setText(preset_name)
|
|
|
|
# Visual feedback
|
|
current_item.setBackground(Qt.darkGreen)
|
|
for i in range(self.preset_list.count()):
|
|
item = self.preset_list.item(i)
|
|
if item != current_item:
|
|
item.setBackground(Qt.transparent)
|
|
|
|
@pyqtSlot()
|
|
def save_new_preset(self):
|
|
"""Save current settings as a new preset"""
|
|
name, ok = QInputDialog.getText(self, "New Preset", "Enter preset name:")
|
|
if ok and name:
|
|
if name in self.presets:
|
|
reply = QMessageBox.question(
|
|
self, "Overwrite Preset",
|
|
f"Preset '{name}' already exists. Overwrite?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
if reply != QMessageBox.Yes:
|
|
return
|
|
|
|
# Capture current settings
|
|
preset_data = self.capture_current_settings()
|
|
preset_data["timestamp"] = self.get_current_timestamp()
|
|
|
|
# Save preset
|
|
self.presets[name] = preset_data
|
|
self.save_preset_to_file(name, preset_data)
|
|
|
|
# Update list
|
|
self.refresh_preset_list()
|
|
|
|
# Select the new preset
|
|
items = self.preset_list.findItems(name, Qt.MatchExactly)
|
|
if items:
|
|
self.preset_list.setCurrentItem(items[0])
|
|
|
|
@pyqtSlot()
|
|
def update_selected_preset(self):
|
|
"""Update the selected preset with current settings"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
preset_name = current_item.text()
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Update Preset",
|
|
f"Update preset '{preset_name}' with current settings?",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
preset_data = self.capture_current_settings()
|
|
preset_data["timestamp"] = self.get_current_timestamp()
|
|
|
|
self.presets[preset_name] = preset_data
|
|
self.save_preset_to_file(preset_name, preset_data)
|
|
|
|
@pyqtSlot()
|
|
def delete_selected_preset(self):
|
|
"""Delete the selected preset"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
preset_name = current_item.text()
|
|
|
|
reply = QMessageBox.question(
|
|
self, "Delete Preset",
|
|
f"Delete preset '{preset_name}'? This cannot be undone.",
|
|
QMessageBox.Yes | QMessageBox.No
|
|
)
|
|
|
|
if reply == QMessageBox.Yes:
|
|
# Remove from memory and file
|
|
if preset_name in self.presets:
|
|
del self.presets[preset_name]
|
|
|
|
preset_file = os.path.join(self.presets_directory, f"{preset_name}.json")
|
|
if os.path.exists(preset_file):
|
|
os.remove(preset_file)
|
|
|
|
# Update list
|
|
self.refresh_preset_list()
|
|
|
|
# Clear current if it was deleted
|
|
if self.current_preset == preset_name:
|
|
self.current_preset = None
|
|
self.current_preset_label.setText("None")
|
|
|
|
@pyqtSlot()
|
|
def rename_selected_preset(self):
|
|
"""Rename the selected preset"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
old_name = current_item.text()
|
|
new_name, ok = QInputDialog.getText(self, "Rename Preset", "Enter new name:", text=old_name)
|
|
|
|
if ok and new_name and new_name != old_name:
|
|
if new_name in self.presets:
|
|
QMessageBox.warning(self, "Rename Error", f"Preset '{new_name}' already exists.")
|
|
return
|
|
|
|
# Move preset data
|
|
self.presets[new_name] = self.presets[old_name]
|
|
del self.presets[old_name]
|
|
|
|
# Handle files
|
|
old_file = os.path.join(self.presets_directory, f"{old_name}.json")
|
|
new_file = os.path.join(self.presets_directory, f"{new_name}.json")
|
|
|
|
if os.path.exists(old_file):
|
|
os.rename(old_file, new_file)
|
|
|
|
# Update current preset reference
|
|
if self.current_preset == old_name:
|
|
self.current_preset = new_name
|
|
self.current_preset_label.setText(new_name)
|
|
|
|
# Refresh list
|
|
self.refresh_preset_list()
|
|
|
|
@pyqtSlot()
|
|
def duplicate_selected_preset(self):
|
|
"""Duplicate the selected preset"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
source_name = current_item.text()
|
|
new_name, ok = QInputDialog.getText(self, "Duplicate Preset", "Enter name for copy:", text=f"{source_name} Copy")
|
|
|
|
if ok and new_name:
|
|
if new_name in self.presets:
|
|
QMessageBox.warning(self, "Duplicate Error", f"Preset '{new_name}' already exists.")
|
|
return
|
|
|
|
# Copy preset data
|
|
self.presets[new_name] = self.presets[source_name].copy()
|
|
self.save_preset_to_file(new_name, self.presets[new_name])
|
|
|
|
# Refresh list and select new preset
|
|
self.refresh_preset_list()
|
|
items = self.preset_list.findItems(new_name, Qt.MatchExactly)
|
|
if items:
|
|
self.preset_list.setCurrentItem(items[0])
|
|
|
|
@pyqtSlot()
|
|
def import_preset(self):
|
|
"""Import a preset from file"""
|
|
file_path, _ = QFileDialog.getOpenFileName(
|
|
self, "Import Preset", "", "JSON Files (*.json);;All Files (*)"
|
|
)
|
|
|
|
if file_path:
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
preset_data = json.load(f)
|
|
|
|
# Get name from user
|
|
default_name = os.path.splitext(os.path.basename(file_path))[0]
|
|
name, ok = QInputDialog.getText(self, "Import Preset", "Preset name:", text=default_name)
|
|
|
|
if ok and name:
|
|
self.presets[name] = preset_data
|
|
self.save_preset_to_file(name, preset_data)
|
|
self.refresh_preset_list()
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Import Error", f"Error importing preset: {str(e)}")
|
|
|
|
@pyqtSlot()
|
|
def export_selected_preset(self):
|
|
"""Export the selected preset to file"""
|
|
current_item = self.preset_list.currentItem()
|
|
if not current_item:
|
|
return
|
|
|
|
preset_name = current_item.text()
|
|
|
|
file_path, _ = QFileDialog.getSaveFileName(
|
|
self, "Export Preset", f"{preset_name}.json", "JSON Files (*.json);;All Files (*)"
|
|
)
|
|
|
|
if file_path:
|
|
try:
|
|
with open(file_path, 'w') as f:
|
|
json.dump(self.presets[preset_name], f, indent=2)
|
|
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Export Error", f"Error exporting preset: {str(e)}")
|
|
|
|
def load_presets_from_directory(self):
|
|
"""Load all presets from the presets directory"""
|
|
if not os.path.exists(self.presets_directory):
|
|
return
|
|
|
|
for filename in os.listdir(self.presets_directory):
|
|
if filename.endswith('.json'):
|
|
preset_name = os.path.splitext(filename)[0]
|
|
file_path = os.path.join(self.presets_directory, filename)
|
|
|
|
try:
|
|
with open(file_path, 'r') as f:
|
|
preset_data = json.load(f)
|
|
self.presets[preset_name] = preset_data
|
|
except Exception as e:
|
|
print(f"Error loading preset {filename}: {e}")
|
|
|
|
self.refresh_preset_list()
|
|
|
|
def save_preset_to_file(self, name: str, preset_data: dict):
|
|
"""Save a preset to file"""
|
|
file_path = os.path.join(self.presets_directory, f"{name}.json")
|
|
try:
|
|
with open(file_path, 'w') as f:
|
|
json.dump(preset_data, f, indent=2)
|
|
except Exception as e:
|
|
QMessageBox.critical(self, "Save Error", f"Error saving preset: {str(e)}")
|
|
|
|
def refresh_preset_list(self):
|
|
"""Refresh the preset list display"""
|
|
self.preset_list.clear()
|
|
for name in sorted(self.presets.keys()):
|
|
item = QListWidgetItem(name)
|
|
if name == self.current_preset:
|
|
item.setBackground(Qt.darkGreen)
|
|
self.preset_list.addItem(item)
|
|
|
|
def get_current_timestamp(self) -> str:
|
|
"""Get current timestamp string"""
|
|
from datetime import datetime
|
|
return datetime.now().isoformat()
|
|
|
|
def new_preset(self):
|
|
"""Create a new preset (for menu action)"""
|
|
self.save_new_preset()
|
|
|
|
def load_preset(self):
|
|
"""Load preset (for menu action)"""
|
|
if self.preset_list.currentItem():
|
|
self.load_selected_preset()
|
|
|
|
def save_preset(self):
|
|
"""Save preset (for menu action)"""
|
|
if self.preset_list.currentItem():
|
|
self.update_selected_preset()
|
|
else:
|
|
self.save_new_preset()
|