""" 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.global_instrument_combo.setMaxVisibleItems(15) # Show more items 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) instrument_combo.setMaxVisibleItems(15) # Show more items 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)