""" 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) 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 = {} # 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) # 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_global_settings(self) -> QGroupBox: """Create global volume/velocity range settings""" group = QGroupBox("Global Volume Range") layout = QGridLayout(group) # Global Volume Range layout.addWidget(QLabel("Volume Range:"), 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 layout.addWidget(QLabel("Velocity Range:"), 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)