""" Volume Controls GUI Interface for tempo-linked volume and brightness pattern controls. """ from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout, QGroupBox, QComboBox, QSlider, QSpinBox, QLabel, QPushButton, QFrame, QScrollArea, QSizePolicy, QCheckBox) from PyQt5.QtCore import Qt, pyqtSlot class VolumeControls(QWidget): """Control panel for tempo-linked volume and brightness patterns""" # Tempo-linked pattern types with bar lengths TEMPO_PATTERNS = { "static": "Static", "1_bar_swell": "1 Bar Swell", "2_bar_swell": "2 Bar Swell", "4_bar_swell": "4 Bar Swell", "8_bar_swell": "8 Bar Swell", "16_bar_swell": "16 Bar Swell", "accent_2": "Accent Every 2nd", "accent_3": "Accent Every 3rd", "accent_4": "Accent Every 4th", "accent_5": "Accent Every 5th", "accent_6": "Accent Every 6th", "accent_7": "Accent Every 7th", "accent_8": "Accent Every 8th", "cascade_up": "Cascade Up", "cascade_down": "Cascade Down", "random": "Random" } def __init__(self, volume_engine): super().__init__() self.volume_engine = volume_engine self.current_pattern = "static" self.armed_pattern_button = None self.pattern_buttons = {} # Override checkboxes for preventing preset changes self.override_checkboxes = {} # Scaling support self.scale_factor = 1.0 self.setup_ui() self.connect_signals() def apply_scaling(self, scale_factor): """Apply new scaling factor to all buttons""" self.scale_factor = scale_factor # Update all pattern buttons with new scaling for button in self.pattern_buttons.values(): self.update_pattern_button_style_with_scale(button, self.get_button_state(button)) def get_button_state(self, button): """Determine button state from current styling""" style = button.styleSheet() if "#2d5a2d" in style or "#00aa44" in style: return "active" elif "#ff8800" in style: return "armed" else: return "inactive" def update_pattern_button_style_with_scale(self, button, state): """Update pattern button styling with current scale factor""" font_size = max(8, int(12 * self.scale_factor)) padding = max(2, int(5 * self.scale_factor)) min_height = max(20, int(30 * self.scale_factor)) # Set size policy for expansion button.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) if state == "active": button.setStyleSheet(f""" QPushButton {{ background: #2d5a2d; color: white; border: 1px solid #4a8a4a; font-weight: bold; font-size: {font_size}px; min-height: {min_height}px; padding: {padding}px; }} QPushButton:hover {{ background: #3d6a3d; border: 1px solid #5aaa5a; }} """) elif state == "armed": button.setStyleSheet(f""" QPushButton {{ background: #ff8800; color: white; border: 1px solid #ffaa00; font-weight: bold; font-size: {font_size}px; min-height: {min_height}px; padding: {padding}px; }} QPushButton:hover {{ background: #ffaa00; border: 1px solid #ffcc33; }} """) else: # inactive button.setStyleSheet(f""" QPushButton {{ background: #3a3a3a; color: #ffffff; border: 1px solid #555555; font-weight: bold; font-size: {font_size}px; min-height: {min_height}px; padding: {padding}px; }} QPushButton:hover {{ background: #505050; border: 1px solid #777777; }} """) def setup_ui(self): """Set up the user interface""" layout = QVBoxLayout(self) # Tempo-Linked Pattern Settings pattern_group = self.create_pattern_settings() layout.addWidget(pattern_group) # Global Range Settings (keep min/max volume and velocity) global_group = self.create_global_settings() layout.addWidget(global_group) layout.addStretch() def create_pattern_settings(self) -> QGroupBox: """Create tempo-linked volume pattern settings""" group = QGroupBox("Tempo-Linked Volume Patterns") layout = QVBoxLayout(group) # Override checkbox for pattern selection pattern_override_label = self.create_parameter_label_with_override("Volume Pattern:", "volume_pattern") layout.addWidget(pattern_override_label) # Description desc = QLabel("Volume changes once per note per channel, linked to arpeggiator tempo") desc.setStyleSheet("color: #888888; font-style: italic;") desc.setWordWrap(True) layout.addWidget(desc) # Pattern buttons pattern_widget = self.create_pattern_buttons() layout.addWidget(pattern_widget) return group def create_pattern_buttons(self) -> QWidget: """Create pattern selection buttons""" widget = QWidget() layout = QGridLayout(widget) layout.setSpacing(3) row = 0 col = 0 for pattern_key, display_name in self.TEMPO_PATTERNS.items(): button = QPushButton(display_name) button.setCheckable(True) button.clicked.connect(lambda checked, p=pattern_key: self.on_pattern_button_clicked(p)) # Set initial state if pattern_key == "static": button.setChecked(True) self.update_pattern_button_style(button, "active") else: self.update_pattern_button_style(button, "inactive") self.pattern_buttons[pattern_key] = button layout.addWidget(button, row, col) col += 1 if col >= 3: # 3 buttons per row col = 0 row += 1 return widget def update_pattern_button_style(self, button, state): """Update pattern button styling based on state""" self.update_pattern_button_style_with_scale(button, state) def create_parameter_label_with_override(self, text, param_name): """Create a label with override checkbox""" container = QWidget() container_layout = QHBoxLayout(container) container_layout.setContentsMargins(0, 0, 0, 0) # Override checkbox checkbox = QCheckBox() checkbox.setToolTip(f"Override {text.lower()} during preset cycling") checkbox.stateChanged.connect(lambda state, param=param_name: self.on_parameter_override_changed(param, state == 2)) self.override_checkboxes[param_name] = checkbox # Label label = QLabel(text) container_layout.addWidget(checkbox) container_layout.addWidget(label) container_layout.addStretch() return container def on_parameter_override_changed(self, param_name, is_overridden): """Handle parameter override checkbox changes""" # No immediate action needed - preset_controls.py will check these states pass def is_parameter_overridden(self, param_name): """Check if a parameter is overridden""" return param_name in self.override_checkboxes and self.override_checkboxes[param_name].isChecked() def create_global_settings(self) -> QGroupBox: """Create global volume/velocity range settings""" group = QGroupBox("Global Volume Range") layout = QGridLayout(group) # Global Volume Range vol_range_label = self.create_parameter_label_with_override("Volume Range:", "volume_range") layout.addWidget(vol_range_label, 0, 0) vol_layout = QVBoxLayout() # Min Volume min_vol_layout = QHBoxLayout() min_vol_layout.addWidget(QLabel("Min:")) self.min_volume_slider = QSlider(Qt.Horizontal) self.min_volume_slider.setRange(0, 100) # 0% to 100% self.min_volume_slider.setValue(10) # 10% for subtle lighting self.min_volume_label = QLabel("10%") self.min_volume_label.setFixedWidth(40) min_vol_layout.addWidget(self.min_volume_slider) min_vol_layout.addWidget(self.min_volume_label) vol_layout.addLayout(min_vol_layout) # Max Volume max_vol_layout = QHBoxLayout() max_vol_layout.addWidget(QLabel("Max:")) self.max_volume_slider = QSlider(Qt.Horizontal) self.max_volume_slider.setRange(0, 100) # 0% to 100% self.max_volume_slider.setValue(100) # 100% self.max_volume_label = QLabel("100%") self.max_volume_label.setFixedWidth(40) max_vol_layout.addWidget(self.max_volume_slider) max_vol_layout.addWidget(self.max_volume_label) vol_layout.addLayout(max_vol_layout) layout.addLayout(vol_layout, 0, 1) # Global Velocity Range vel_range_label = self.create_parameter_label_with_override("Velocity Range:", "velocity_range") layout.addWidget(vel_range_label, 1, 0) vel_layout = QVBoxLayout() # Min Velocity min_vel_layout = QHBoxLayout() min_vel_layout.addWidget(QLabel("Min:")) self.min_velocity_slider = QSlider(Qt.Horizontal) self.min_velocity_slider.setRange(1, 127) self.min_velocity_slider.setValue(40) self.min_velocity_label = QLabel("40") self.min_velocity_label.setFixedWidth(40) min_vel_layout.addWidget(self.min_velocity_slider) min_vel_layout.addWidget(self.min_velocity_label) vel_layout.addLayout(min_vel_layout) # Max Velocity max_vel_layout = QHBoxLayout() max_vel_layout.addWidget(QLabel("Max:")) self.max_velocity_slider = QSlider(Qt.Horizontal) self.max_velocity_slider.setRange(1, 127) self.max_velocity_slider.setValue(127) self.max_velocity_label = QLabel("127") self.max_velocity_label.setFixedWidth(40) max_vel_layout.addWidget(self.max_velocity_slider) max_vel_layout.addWidget(self.max_velocity_label) vel_layout.addLayout(max_vel_layout) layout.addLayout(vel_layout, 1, 1) return group def connect_signals(self): """Connect GUI controls to volume engine""" # Volume range controls self.min_volume_slider.valueChanged.connect(self.on_min_volume_changed) self.max_volume_slider.valueChanged.connect(self.on_max_volume_changed) self.min_velocity_slider.valueChanged.connect(self.on_min_velocity_changed) self.max_velocity_slider.valueChanged.connect(self.on_max_velocity_changed) def on_pattern_button_clicked(self, pattern): """Handle pattern button click""" # Note: We'll need to modify this to work with arpeggiator playing state # For now, apply immediately self.set_active_pattern(pattern) # Reset pattern position when changing patterns self.volume_engine.reset_pattern() # Map our tempo patterns to volume engine patterns if pattern == "static": self.volume_engine.set_pattern("static") elif "swell" in pattern: # Pass the full pattern name to the volume engine for multi-bar support self.volume_engine.set_pattern(pattern) # Set appropriate speed based on bar length if "1_bar" in pattern: self.volume_engine.set_pattern_speed(2.0) # Faster for 1 bar elif "2_bar" in pattern: self.volume_engine.set_pattern_speed(1.0) # Normal speed elif "4_bar" in pattern: self.volume_engine.set_pattern_speed(0.5) # Slower for 4 bars elif "8_bar" in pattern: self.volume_engine.set_pattern_speed(0.25) # Very slow for 8 bars elif "16_bar" in pattern: self.volume_engine.set_pattern_speed(0.125) # Extra slow for 16 bars elif "accent_" in pattern: # Pass the full pattern name for accent patterns self.volume_engine.set_pattern(pattern) elif pattern == "random": self.volume_engine.set_pattern("random") elif pattern == "cascade_up": self.volume_engine.set_pattern("cascade") elif pattern == "cascade_down": self.volume_engine.set_pattern("cascade") def set_active_pattern(self, pattern): """Set active pattern button""" # Clear current active state if self.current_pattern in self.pattern_buttons: self.update_pattern_button_style(self.pattern_buttons[self.current_pattern], "inactive") # Set new active state self.current_pattern = pattern self.update_pattern_button_style(self.pattern_buttons[pattern], "active") @pyqtSlot(int) def on_min_volume_changed(self, value): """Handle minimum volume change""" # Ensure min doesn't exceed max if value >= self.max_volume_slider.value(): value = self.max_volume_slider.value() - 1 self.min_volume_slider.setValue(value) self.min_volume_label.setText(f"{value}%") min_vol = value / 100.0 max_vol = self.max_volume_slider.value() / 100.0 self.volume_engine.set_global_ranges(min_vol, max_vol, self.min_velocity_slider.value(), self.max_velocity_slider.value()) @pyqtSlot(int) def on_max_volume_changed(self, value): """Handle maximum volume change""" # Ensure max doesn't go below min if value <= self.min_volume_slider.value(): value = self.min_volume_slider.value() + 1 self.max_volume_slider.setValue(value) self.max_volume_label.setText(f"{value}%") min_vol = self.min_volume_slider.value() / 100.0 max_vol = value / 100.0 self.volume_engine.set_global_ranges(min_vol, max_vol, self.min_velocity_slider.value(), self.max_velocity_slider.value()) @pyqtSlot(int) def on_min_velocity_changed(self, value): """Handle minimum velocity change""" # Ensure min doesn't exceed max if value >= self.max_velocity_slider.value(): value = self.max_velocity_slider.value() - 1 self.min_velocity_slider.setValue(value) self.min_velocity_label.setText(str(value)) min_vol = self.min_volume_slider.value() / 100.0 max_vol = self.max_volume_slider.value() / 100.0 self.volume_engine.set_global_ranges(min_vol, max_vol, value, self.max_velocity_slider.value()) @pyqtSlot(int) def on_max_velocity_changed(self, value): """Handle maximum velocity change""" # Ensure max doesn't go below min if value <= self.min_velocity_slider.value(): value = self.min_velocity_slider.value() + 1 self.max_velocity_slider.setValue(value) self.max_velocity_label.setText(str(value)) min_vol = self.min_volume_slider.value() / 100.0 max_vol = self.max_volume_slider.value() / 100.0 self.volume_engine.set_global_ranges(min_vol, max_vol, self.min_velocity_slider.value(), value)