""" Mapper module for MIDI-to-Hue application. Handles the mapping logic between MIDI messages and Hue light controls. """ from typing import Dict, Any, Optional import mido class MidiToHueMapper: """Maps MIDI messages to Hue light commands according to configuration.""" def __init__(self, hue_controller, midi_controller, mappings: list): """ Initialize the MIDI-to-Hue mapper. Args: hue_controller: HueController instance midi_controller: MidiController instance mappings: List of mapping dictionaries from config """ self.hue_controller = hue_controller self.midi_controller = midi_controller self.mappings = mappings self.last_note_hit_light_name = None # Register as a handler for MIDI messages self.midi_controller.register_handler(self.process_midi_message) def process_midi_message(self, msg) -> None: """ Process a MIDI message and update lights if it matches any mappings. Args: msg: MIDI message object from mido """ # Extract MIDI message properties if not hasattr(msg, 'type') or not hasattr(msg, 'channel'): return # Skip messages without required attributes # Extract value (could be in different attributes depending on message type) default_value = None if hasattr(msg, 'value'): default_value = msg.value elif hasattr(msg, 'velocity'): default_value = msg.velocity # Get the input name for this message if available input_name = None if hasattr(self.midi_controller, 'get_input_name'): input_name = self.midi_controller.get_input_name(msg) # Process message against our mappings for mapping in self.mappings: # Match by input_name if available, otherwise fall back to channel/control matching if input_name and 'input_name' in mapping: if input_name != mapping['input_name']: continue elif msg.type == "note_on" and 'midi_channel' in mapping and 'midi_control' in mapping: if msg.channel != mapping['midi_channel'] or (hasattr(msg, 'note') and msg.note != mapping['midi_control']): continue elif msg.type == "control_change" and 'midi_channel' in mapping and 'midi_control' in mapping: if msg.channel != mapping['midi_channel'] or (hasattr(msg, 'control') and msg.control != mapping['midi_control']): continue else: continue # No match criteria found # Determine the light name light_name = self._resolve_light_name(mapping, msg) if light_name is None: print("No light name specified, skipping update.") continue # Update last note hit for potential use in value_transform if msg.type == "note_on": self.last_note_hit_light_name = light_name # Verify the light exists light = self.hue_controller.get_light_by_name(light_name) if not light: continue # Apply value transformation transformed_value = self._transform_value(mapping, msg, light, default_value) if transformed_value is None: print("Skipping update due to None value") continue # Update LED feedback if needed if msg.type == "note_on" and mapping.get('parameter') == 'on': self._send_led_feedback(msg, transformed_value) # Schedule the light update self.hue_controller.update_light( light_name, mapping['parameter'], transformed_value ) print(f"MIDI: {msg} → {light_name}.{mapping['parameter']} = {transformed_value}") def _resolve_light_name(self, mapping: Dict[str, Any], msg) -> Optional[str]: """ Resolve the light name based on mapping configuration. Args: mapping: Mapping configuration dictionary msg: MIDI message object Returns: Resolved light name or None if not resolved """ if "light_name_eval" in mapping: try: return eval(mapping['light_name_eval'], { 'msg': msg, 'last_note_hit_light_name': self.last_note_hit_light_name, }) except Exception as e: print(f"Error evaluating light_name_eval: {e}") return None return mapping.get('light_name') def _transform_value(self, mapping: Dict[str, Any], msg, light, default_value: Any) -> Any: """ Transform a MIDI value according to mapping configuration. Args: mapping: Mapping configuration dictionary msg: MIDI message object light: Light object from hue controller default_value: Default value from MIDI message Returns: Transformed value or None if transformation failed """ if 'value_transform' not in mapping: return default_value try: return eval(mapping['value_transform'], { 'value': default_value, 'msg': msg, 'light': light, }) except Exception as e: print(f"Error transforming value: {e}") return default_value def _send_led_feedback(self, msg, value: Any) -> None: """ Send LED feedback to the MIDI device. Args: msg: Original MIDI message value: Transformed value (usually boolean) """ try: led_on = mido.Message('note_on', note=msg.note, velocity=127 if value else 0, channel=msg.channel) self.midi_controller.send_message(led_on) except Exception as e: print(f"Error sending LED feedback: {e}")