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