163 lines
6.3 KiB
Python
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}")
|