ship-controller/mapper.py
2025-06-30 01:52:10 +02:00

163 lines
6.3 KiB
Python

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