Add MIDI controller dropdown selection

- Add MIDI port detection and listing functionality
- Create dropdown widget for MIDI controller selection in GUI
- Implement dynamic MIDI controller switching without restart
- Add refresh button to scan for new MIDI devices
- Add status indicator showing connection status
- Add comprehensive error handling for MIDI operations
- Fix initialization timing issues with GUI widgets
- Support graceful fallback when no MIDI devices available
This commit is contained in:
2025-09-27 23:11:42 +12:00
parent e5cf15d7b0
commit f9c3d08b0f

View File

@@ -5,6 +5,7 @@ import json
from async_tkinter_loop import async_handler, async_mainloop
from networking import WebSocketClient
import color_utils
import mido # Import mido for MIDI port detection
import time
from midi import MidiHandler # Import MidiHandler
@@ -33,15 +34,14 @@ class App:
self.websocket_client = WebSocketClient("ws://192.168.4.1:80/ws")
self.root.after(100, async_handler(self.websocket_client.connect))
# --- MIDI Handler ---
MIDI_PORT_INDEX = 1 # Adjust as needed
WEBSOCKET_SERVER_URI = "ws://192.168.4.1:80/ws"
print(f"Initializing MIDI handler with port index {MIDI_PORT_INDEX}")
self.midi_handler = MidiHandler(MIDI_PORT_INDEX, WEBSOCKET_SERVER_URI)
print("MIDI handler initialized")
# --- MIDI Configuration ---
self.available_midi_ports = self.get_midi_ports()
self.current_midi_port_index = 0 # Default to first port
self.midi_handler: MidiHandler | None = None
self.midi_task: asyncio.Task | None = None
# Start MIDI in background
self.root.after(0, async_handler(self.start_midi))
# Initialize MIDI handler with first available port (will be done after GUI is created)
self.pending_midi_init = True if self.available_midi_ports else False
# Configure ttk style (unchanged)
style = ttk.Style()
@@ -54,6 +54,48 @@ class App:
# (Status box removed per request)
# MIDI Controller Selection
midi_frame = ttk.LabelFrame(self.root, text="MIDI Controller")
midi_frame.pack(padx=16, pady=8, fill="x")
# MIDI port dropdown
self.midi_port_var = tk.StringVar()
if self.available_midi_ports:
self.midi_port_var.set(self.available_midi_ports[0])
midi_dropdown = ttk.Combobox(
midi_frame,
textvariable=self.midi_port_var,
values=self.available_midi_ports,
state="readonly",
font=("Arial", 12)
)
midi_dropdown.pack(padx=8, pady=4, fill="x")
midi_dropdown.bind("<<ComboboxSelected>>", self.on_midi_port_change)
# Refresh MIDI ports button
refresh_button = ttk.Button(
midi_frame,
text="Refresh MIDI Ports",
command=self.refresh_midi_ports
)
refresh_button.pack(padx=8, pady=4)
# MIDI connection status
self.midi_status_label = tk.Label(
midi_frame,
text="Status: Disconnected",
bg=bg_color,
fg="red",
font=("Arial", 10)
)
self.midi_status_label.pack(padx=8, pady=2)
# Initialize MIDI handler now that GUI is ready
if self.pending_midi_init:
self.initialize_midi_handler()
self.pending_midi_init = False
# Controls overview (dials grid left, buttons grids right)
controls_frame = ttk.Frame(self.root)
controls_frame.pack(padx=16, pady=8, fill="both")
@@ -188,11 +230,108 @@ class App:
async_mainloop(self.root)
def get_midi_ports(self):
"""Get list of available MIDI input ports"""
try:
port_names = mido.get_input_names()
return port_names
except Exception as e:
print(f"Error getting MIDI ports: {e}")
return []
def initialize_midi_handler(self):
"""Initialize MIDI handler with current port"""
if not self.available_midi_ports:
print("No MIDI ports available")
return
try:
WEBSOCKET_SERVER_URI = "ws://192.168.4.1:80/ws"
print(f"Initializing MIDI handler with port index {self.current_midi_port_index}")
self.midi_handler = MidiHandler(self.current_midi_port_index, WEBSOCKET_SERVER_URI)
print("MIDI handler initialized")
# Update status
port_name = self.available_midi_ports[self.current_midi_port_index]
if hasattr(self, 'midi_status_label'):
self.midi_status_label.config(text=f"Status: Connected to {port_name}", fg="green")
# Start MIDI in background
self.root.after(0, async_handler(self.start_midi))
except Exception as e:
print(f"Error initializing MIDI handler: {e}")
messagebox.showerror("MIDI Error", f"Failed to initialize MIDI handler:\n{e}")
self.midi_handler = None
if hasattr(self, 'midi_status_label'):
self.midi_status_label.config(text="Status: Error", fg="red")
def refresh_midi_ports(self):
"""Refresh the list of available MIDI ports"""
old_ports = self.available_midi_ports.copy()
self.available_midi_ports = self.get_midi_ports()
# Update dropdown values
midi_dropdown = None
for child in self.root.winfo_children():
if isinstance(child, ttk.LabelFrame) and child.cget("text") == "MIDI Controller":
for widget in child.winfo_children():
if isinstance(widget, ttk.Combobox):
midi_dropdown = widget
break
break
if midi_dropdown:
midi_dropdown['values'] = self.available_midi_ports
if self.available_midi_ports and self.midi_port_var.get() not in self.available_midi_ports:
# Current selection is no longer available, select first available
self.midi_port_var.set(self.available_midi_ports[0])
self.current_midi_port_index = 0
self.restart_midi_handler()
elif not self.available_midi_ports:
# No ports available
self.midi_port_var.set("No MIDI ports found")
if hasattr(self, 'midi_status_label'):
self.midi_status_label.config(text="Status: No MIDI ports available", fg="orange")
# Stop current MIDI handler if running
if self.midi_task and not self.midi_task.done():
self.midi_task.cancel()
self.midi_handler = None
print(f"MIDI ports refreshed. Available: {self.available_midi_ports}")
def on_midi_port_change(self, event):
"""Handle MIDI port selection change"""
selected_port = self.midi_port_var.get()
if selected_port in self.available_midi_ports:
self.current_midi_port_index = self.available_midi_ports.index(selected_port)
print(f"MIDI port changed to: {selected_port} (index: {self.current_midi_port_index})")
self.restart_midi_handler()
@async_handler
async def restart_midi_handler(self):
"""Restart MIDI handler with new port"""
try:
# Stop current MIDI task
if self.midi_task and not self.midi_task.done():
self.midi_task.cancel()
try:
await self.midi_task
except asyncio.CancelledError:
pass
# Initialize new MIDI handler
self.initialize_midi_handler()
except Exception as e:
print(f"Error restarting MIDI handler: {e}")
messagebox.showerror("MIDI Error", f"Failed to restart MIDI handler:\n{e}")
@async_handler
async def start_midi(self):
# Launch MidiHandler.run() as a background task
if self.midi_task is None or self.midi_task.done():
if self.midi_handler and (self.midi_task is None or self.midi_task.done()):
self.midi_task = asyncio.create_task(self.midi_handler.run())
elif not self.midi_handler:
print("Cannot start MIDI: no MIDI handler available")
def on_closing(self):
print("Closing application...")
@@ -207,6 +346,38 @@ class App:
await asyncio.sleep(0) # Yield control
def update_status_labels(self):
# Check if MIDI handler is available
if not self.midi_handler:
# Update dial displays with placeholder values
placeholders = [
("n3", "-"), ("Delay", "-"),
("n1", "-"), ("n2", "-"),
("B", "-"), ("Brightness", "-"),
("R", "-"), ("G", "-"),
]
for idx, (label, value) in enumerate(placeholders):
if idx < len(self.dials_boxes):
self.dials_boxes[idx].config(text=f"{label}\n{value}")
# Update knobs with placeholder values
knob_placeholders = [
("CC44", "-"), ("CC45", "-"),
("Rad n1", "-"), ("Rad delay", "-"),
("Alt n1", "-"), ("Alt n2", "-"),
("Pulse n1", "-"), ("Pulse n2", "-"),
]
for idx, (label, value) in enumerate(knob_placeholders):
if idx < len(self.knobs_boxes):
self.knobs_boxes[idx].config(text=f"{label}\n{value}")
# Update buttons with no selection
for lbl in self.button1_cells:
lbl.config(text="", bg=bg_color)
# Reschedule
self.root.after(200, self.update_status_labels)
return
# Pull values from midi_handler
delay = self.midi_handler.delay
brightness = self.midi_handler.brightness