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.
 

317 lines
12 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)
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)