161 lines
5.5 KiB
Python
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
|