""" 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) 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", "1_bar_breathing": "1 Bar Breathing", "2_bar_breathing": "2 Bar Breathing", "4_bar_breathing": "4 Bar Breathing", "1_bar_wave": "1 Bar Wave", "2_bar_wave": "2 Bar Wave", "4_bar_wave": "4 Bar Wave", "cascade_up": "Cascade Up", "cascade_down": "Cascade Down", "random_sparkle": "Random Sparkle" } def __init__(self, volume_engine): super().__init__() self.volume_engine = volume_engine self.current_pattern = "static" self.armed_pattern_button = None self.pattern_buttons = {} self.setup_ui() self.connect_signals() 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""" if state == "active": button.setStyleSheet(""" QPushButton { background-color: #2d5a2d; color: white; border: 2px solid #4a8a4a; font-weight: bold; min-height: 30px; padding: 5px 10px; } """) elif state == "armed": button.setStyleSheet(""" QPushButton { background-color: #5a4d2d; color: white; border: 2px solid #8a7a4a; font-weight: bold; min-height: 30px; padding: 5px 10px; } """) else: # inactive button.setStyleSheet(""" QPushButton { min-height: 30px; padding: 5px 10px; } """) 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: self.volume_engine.set_pattern("swell") # 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 "breathing" in pattern: self.volume_engine.set_pattern("breathing") if "1_bar" in pattern: self.volume_engine.set_pattern_speed(2.0) elif "2_bar" in pattern: self.volume_engine.set_pattern_speed(1.0) elif "4_bar" in pattern: self.volume_engine.set_pattern_speed(0.5) elif "wave" in pattern: self.volume_engine.set_pattern("wave") if "1_bar" in pattern: self.volume_engine.set_pattern_speed(2.0) elif "2_bar" in pattern: self.volume_engine.set_pattern_speed(1.0) elif "4_bar" in pattern: self.volume_engine.set_pattern_speed(0.5) elif pattern == "cascade_up": self.volume_engine.set_pattern("cascade") elif pattern == "cascade_down": self.volume_engine.set_pattern("cascade") elif pattern == "random_sparkle": self.volume_engine.set_pattern("random_sparkle") 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)