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.
 

400 lines
16 KiB

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