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.
361 lines
14 KiB
361 lines
14 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)
|
|
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)
|