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.
 

233 lines
9.2 KiB

"""
Channel Controls GUI
Interface for managing MIDI channels, instruments, and synth configuration.
"""
from PyQt5.QtWidgets import (QWidget, QVBoxLayout, QHBoxLayout, QGridLayout,
QGroupBox, QComboBox, QSpinBox, QLabel, QPushButton,
QScrollArea, QFrame)
from PyQt5.QtCore import Qt, pyqtSlot
class ChannelControls(QWidget):
"""Control panel for MIDI channel management"""
def __init__(self, channel_manager, output_manager):
super().__init__()
self.channel_manager = channel_manager
self.output_manager = output_manager
self.channel_widgets = {}
self.setup_ui()
self.connect_signals()
def setup_ui(self):
"""Set up the user interface"""
layout = QVBoxLayout(self)
# Global Settings
global_group = self.create_global_settings()
layout.addWidget(global_group)
# Individual Channel Settings
channels_group = self.create_channel_settings()
layout.addWidget(channels_group)
def create_global_settings(self) -> QGroupBox:
"""Create global channel settings"""
group = QGroupBox("Global Settings")
layout = QGridLayout(group)
# Active Synth Count
layout.addWidget(QLabel("Active Synths:"), 0, 0)
self.synth_count_spin = QSpinBox()
self.synth_count_spin.setRange(1, 16)
self.synth_count_spin.setValue(8)
layout.addWidget(self.synth_count_spin, 0, 1)
# Global Instrument
layout.addWidget(QLabel("Global Instrument:"), 1, 0)
global_layout = QHBoxLayout()
self.global_instrument_combo = QComboBox()
self.populate_instrument_combo(self.global_instrument_combo)
self.apply_global_button = QPushButton("Apply to All")
global_layout.addWidget(self.global_instrument_combo)
global_layout.addWidget(self.apply_global_button)
layout.addLayout(global_layout, 1, 1)
return group
def create_channel_settings(self) -> QGroupBox:
"""Create individual channel settings"""
group = QGroupBox("Individual Channels")
layout = QVBoxLayout(group)
# Scroll area for channel controls
scroll = QScrollArea()
scroll.setWidgetResizable(True)
scroll.setMaximumHeight(400)
# Widget to contain all channel controls
scroll_widget = QWidget()
scroll_layout = QVBoxLayout(scroll_widget)
# Create controls for each channel
for channel in range(1, 17):
channel_widget = self.create_single_channel_control(channel)
scroll_layout.addWidget(channel_widget)
self.channel_widgets[channel] = channel_widget
scroll.setWidget(scroll_widget)
layout.addWidget(scroll)
return group
def create_single_channel_control(self, channel: int) -> QFrame:
"""Create controls for a single channel"""
frame = QFrame()
frame.setFrameStyle(QFrame.Box)
layout = QHBoxLayout(frame)
# Channel label
channel_label = QLabel(f"Ch {channel}:")
channel_label.setFixedWidth(40)
channel_label.setStyleSheet("font-weight: bold;")
layout.addWidget(channel_label)
# Instrument selection
instrument_combo = QComboBox()
instrument_combo.setFixedWidth(200)
self.populate_instrument_combo(instrument_combo)
layout.addWidget(instrument_combo)
# Voice count display
voice_label = QLabel("Voices: 0/3")
voice_label.setFixedWidth(80)
layout.addWidget(voice_label)
# Status indicator
status_label = QLabel("")
status_label.setStyleSheet("color: #666666; font-size: 16px;")
status_label.setFixedWidth(20)
layout.addWidget(status_label)
layout.addStretch()
# Store references for easy access
frame.instrument_combo = instrument_combo
frame.voice_label = voice_label
frame.status_label = status_label
frame.channel = channel
# Connect instrument change
instrument_combo.currentIndexChanged.connect(
lambda idx, ch=channel: self.on_channel_instrument_changed(ch, idx)
)
return frame
def populate_instrument_combo(self, combo: QComboBox):
"""Populate combo box with GM instruments"""
for i, name in enumerate(self.channel_manager.GM_PROGRAMS):
combo.addItem(f"{i:03d}: {name}", i)
def connect_signals(self):
"""Connect signals and slots"""
# Global settings
self.synth_count_spin.valueChanged.connect(self.on_synth_count_changed)
self.apply_global_button.clicked.connect(self.on_apply_global_instrument)
# Channel manager signals
self.channel_manager.active_synth_count_changed.connect(self.on_active_count_changed)
self.channel_manager.channel_instrument_changed.connect(self.on_instrument_changed)
self.channel_manager.voice_allocation_changed.connect(self.on_voice_allocation_changed)
@pyqtSlot(int)
def on_synth_count_changed(self, count):
"""Handle active synth count change"""
self.channel_manager.set_active_synth_count(count)
self.update_channel_visibility()
@pyqtSlot()
def on_apply_global_instrument(self):
"""Apply global instrument to all active channels"""
program = self.global_instrument_combo.currentData()
if program is not None:
self.channel_manager.set_all_instruments(program)
# Send program changes via output manager
for channel in self.channel_manager.get_active_channels():
self.output_manager.send_program_change(channel, program)
@pyqtSlot(int, int)
def on_channel_instrument_changed(self, channel, combo_index):
"""Handle individual channel instrument change"""
combo = self.channel_widgets[channel].instrument_combo
program = combo.itemData(combo_index)
if program is not None:
self.channel_manager.set_channel_instrument(channel, program)
self.output_manager.send_program_change(channel, program)
@pyqtSlot(int)
def on_active_count_changed(self, count):
"""Handle active synth count change from channel manager"""
self.synth_count_spin.setValue(count)
self.update_channel_visibility()
@pyqtSlot(int, int)
def on_instrument_changed(self, channel, program):
"""Handle instrument change from channel manager"""
if channel in self.channel_widgets:
combo = self.channel_widgets[channel].instrument_combo
# Find and select the correct item
for i in range(combo.count()):
if combo.itemData(i) == program:
combo.setCurrentIndex(i)
break
@pyqtSlot(int, list)
def on_voice_allocation_changed(self, channel, active_notes):
"""Handle voice allocation change"""
if channel in self.channel_widgets:
voice_count = len(active_notes)
max_voices = self.channel_manager.max_voices_per_synth
voice_label = self.channel_widgets[channel].voice_label
voice_label.setText(f"Voices: {voice_count}/{max_voices}")
# Update status indicator
status_label = self.channel_widgets[channel].status_label
if voice_count > 0:
if voice_count >= max_voices:
status_label.setStyleSheet("color: #aa6600; font-size: 16px;") # Orange - full
else:
status_label.setStyleSheet("color: #00aa00; font-size: 16px;") # Green - active
else:
status_label.setStyleSheet("color: #666666; font-size: 16px;") # Gray - inactive
def update_channel_visibility(self):
"""Update visibility of channel controls based on active count"""
active_count = self.channel_manager.active_synth_count
for channel, widget in self.channel_widgets.items():
if channel <= active_count:
widget.show()
widget.setStyleSheet("") # Active appearance
else:
widget.hide()
# Could also use different styling instead of hiding
# widget.setStyleSheet("color: #666666;") # Grayed out
def refresh_all_channels(self):
"""Refresh all channel displays"""
for channel in range(1, 17):
if channel in self.channel_widgets:
# Update instrument display
program = self.channel_manager.get_channel_instrument(channel)
if program is not None:
combo = self.channel_widgets[channel].instrument_combo
for i in range(combo.count()):
if combo.itemData(i) == program:
combo.setCurrentIndex(i)
break
# Update voice count
voices = self.channel_manager.get_active_voices(channel)
self.on_voice_allocation_changed(channel, voices)