ship-controller/midi_controller.py
2025-06-30 02:08:43 +02:00

161 lines
5.5 KiB
Python

"""
MIDI Controller module for MIDI-to-Hue application.
Handles MIDI device connections, message processing, and event handling.
"""
import mido
from typing import Dict, List, Callable, Optional, Any, Union
class MidiController:
"""Manages MIDI device connections and message handling."""
def __init__(self, midi_device_index: int = 1):
"""
Initialize the MIDI controller.
Args:
midi_device_index: Index of the MIDI device to use
"""
self.midi_device_index = midi_device_index
self.port = None
self.input_names = []
self.output_names = []
self.ioport_names = []
self.message_handlers = []
# Initialize MIDI
self._initialize()
def _initialize(self) -> None:
"""Initialize MIDI ports and discover available devices."""
# Get available MIDI inputs/outputs
self.input_names = mido.get_input_names()
self.output_names = mido.get_output_names()
self.ioport_names = mido.get_ioport_names()
def list_devices(self) -> None:
"""Print available MIDI devices."""
print("\nAvailable MIDI inputs:")
for i, name in enumerate(self.input_names):
print(f" {i}: {name}")
print("\nAvailable MIDI outputs:")
for i, name in enumerate(self.output_names):
print(f" {i}: {name}")
print("\nAvailable MIDI I/O ports:")
for i, name in enumerate(self.ioport_names):
print(f" {i}: {name}")
def open(self) -> bool:
"""Open the configured MIDI port."""
try:
if not self.input_names:
print("No MIDI inputs found. Please connect a MIDI device.")
return False
if len(self.input_names) <= self.midi_device_index:
print(f"Error: MIDI device index {self.midi_device_index} out of range.")
print(f"Available devices: {len(self.input_names)}")
return False
selected_input = self.input_names[self.midi_device_index]
print(f"\nOpening MIDI input: {selected_input}")
self.port = mido.open_ioport(selected_input)
return True
except Exception as e:
print(f"Error opening MIDI device: {e}")
return False
def close(self) -> None:
"""Close the MIDI port if open."""
if self.port:
self.port.close()
self.port = None
def register_handler(self, handler: Callable) -> None:
"""Register a function to handle incoming MIDI messages."""
self.message_handlers.append(handler)
def send_message(self, msg) -> None:
"""Send a MIDI message through the current port."""
if self.port:
self.port.send(msg)
else:
print("Error: No MIDI port open")
def process_messages(self) -> None:
"""Start processing MIDI messages, calling registered handlers."""
if not self.port:
print("Error: No MIDI port open")
return
print("Waiting for MIDI messages... Press Ctrl+C to exit.")
try:
for msg in self.port:
print(f"Received: {msg}")
# Call all registered handlers
for handler in self.message_handlers:
handler(msg)
except KeyboardInterrupt:
print("\nExiting MIDI processing...")
except Exception as e:
print(f"Error processing MIDI messages: {e}")
class DeviceMappingManager:
"""Manages device-specific MIDI mappings and message types."""
def __init__(self, device_mappings: Dict[str, Dict[str, str]] = None):
"""
Initialize with device mappings.
Args:
device_mappings: Dictionary mapping devices to their control mappings
"""
self.device_mappings = device_mappings or {}
self.connected_device = None
def set_active_device(self, device_name: str) -> bool:
"""
Set the active MIDI device mapping.
Args:
device_name: Name of the device to activate
Returns:
True if device was found and set, False otherwise
"""
if device_name in self.device_mappings:
self.connected_device = self.device_mappings[device_name]
print(f"Activated mapping for device: {device_name}")
return True
else:
print(f"No mapping found for device: {device_name}")
self.connected_device = {}
return False
def get_input_name(self, msg) -> Optional[str]:
"""
Get the mapped input name for a MIDI message.
Args:
msg: MIDI message object
Returns:
Mapped input name or None if no mapping found
"""
if not self.connected_device or not hasattr(msg, 'type') or not hasattr(msg, 'channel'):
return None
message_key = None
if msg.type == "note_on" and hasattr(msg, 'note'):
message_key = f"note_on/{msg.channel}/{msg.note}"
elif msg.type == "control_change" and hasattr(msg, 'control'):
message_key = f"control_change/{msg.channel}/{msg.control}"
if message_key and message_key in self.connected_device:
return self.connected_device[message_key]
return None