2138 lines
73 KiB
Python
2138 lines
73 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
hardware_integration.py - Mapeo de Hardware MIDI & Sensores (T166-T180)
|
|
|
|
BLOQUE 3: Integración completa de controladores MIDI hardware para
|
|
performance en vivo, incluyendo mapeo de CC, feedback luminoso,
|
|
sincronización MIDI Clock y modos de performance especializados.
|
|
|
|
Soporta:
|
|
- Allen & Heath Xone:K2
|
|
- AKAI APC40/APC40 MKII
|
|
- Pioneer DDJ series (mapeo MIDI estándar)
|
|
- Controladores MIDI genéricos
|
|
|
|
Author: AbletonMCP-AI System
|
|
Version: 1.0.0
|
|
"""
|
|
|
|
import asyncio
|
|
import json
|
|
import logging
|
|
import math
|
|
import threading
|
|
import time
|
|
from dataclasses import dataclass, field
|
|
from enum import Enum
|
|
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
|
|
from collections import deque
|
|
|
|
try:
|
|
import mido
|
|
from mido import Message, MidiFile, MidiTrack
|
|
MIDO_AVAILABLE = True
|
|
except ImportError:
|
|
MIDO_AVAILABLE = False
|
|
|
|
# Configuración de logging
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# =============================================================================
|
|
# T166: Configuración de Hardware y Mapeos MIDI
|
|
# =============================================================================
|
|
|
|
class HardwareType(Enum):
|
|
"""Tipos de controladores soportados."""
|
|
XONE_K2 = "xone_k2"
|
|
AKAI_APC40 = "akai_apc40"
|
|
AKAI_APC40_MK2 = "akai_apc40_mk2"
|
|
PIONEER_DDJ = "pioneer_ddj"
|
|
GENERIC_MIDI = "generic_midi"
|
|
|
|
|
|
@dataclass
|
|
class CCMapping:
|
|
"""Mapeo de un control CC MIDI a una función."""
|
|
cc_number: int
|
|
channel: int
|
|
name: str
|
|
min_value: int = 0
|
|
max_value: int = 127
|
|
curve: str = "linear" # linear, exponential, s_curve
|
|
target_bus: Optional[str] = None
|
|
callback: Optional[Callable] = None
|
|
async_callback: Optional[Callable] = None
|
|
feedback_enabled: bool = False
|
|
led_output_cc: Optional[int] = None
|
|
|
|
|
|
@dataclass
|
|
class NoteMapping:
|
|
"""Mapeo de nota MIDI a una función."""
|
|
note: int
|
|
channel: int
|
|
name: str
|
|
trigger_mode: str = "toggle" # toggle, momentary, hold
|
|
callback: Optional[Callable] = None
|
|
async_callback: Optional[Callable] = None
|
|
feedback_enabled: bool = False
|
|
led_color: int = 0 # Para APC40: 0=off, 1=green, 2=red, 3=yellow
|
|
|
|
|
|
@dataclass
|
|
class HardwareConfig:
|
|
"""Configuración completa de un controlador hardware."""
|
|
hardware_type: HardwareType
|
|
name: str
|
|
input_port: str = ""
|
|
output_port: str = ""
|
|
cc_mappings: Dict[str, CCMapping] = field(default_factory=dict)
|
|
note_mappings: Dict[str, NoteMapping] = field(default_factory=dict)
|
|
has_led_feedback: bool = False
|
|
has_7segment_display: bool = False
|
|
has_lcd_display: bool = False
|
|
fader_count: int = 0
|
|
knob_count: int = 0
|
|
pad_count: int = 0
|
|
button_count: int = 0
|
|
|
|
|
|
# Mapeos predefinidos para Xone:K2
|
|
XONE_K2_MAPPINGS = {
|
|
"cc_mappings": {
|
|
"filter_high": CCMapping(1, 0, "Filter High", 0, 127, "linear", "music_bus"),
|
|
"filter_mid": CCMapping(2, 0, "Filter Mid", 0, 127, "linear", "music_bus"),
|
|
"filter_low": CCMapping(3, 0, "Filter Low", 0, 127, "linear", "music_bus"),
|
|
"gain_staging": CCMapping(4, 0, "Gain Staging", 0, 127, "linear"),
|
|
"humanize_amount": CCMapping(5, 0, "Humanize", 0, 127, "exponential"),
|
|
"sidechain_amount": CCMapping(6, 0, "Sidechain", 0, 127, "s_curve", "bass_bus"),
|
|
"reverb_send": CCMapping(7, 0, "Reverb Send", 0, 127, "exponential", "music_bus"),
|
|
"delay_send": CCMapping(8, 0, "Delay Send", 0, 127, "exponential", "music_bus"),
|
|
"fader_1": CCMapping(11, 0, "Fader 1", 0, 127, "linear", "drums_bus"),
|
|
"fader_2": CCMapping(12, 0, "Fader 2", 0, 127, "linear", "bass_bus"),
|
|
"fader_3": CCMapping(13, 0, "Fader 3", 0, 127, "linear", "music_bus"),
|
|
"fader_4": CCMapping(14, 0, "Fader 4", 0, 127, "linear", "master"),
|
|
"master_volume": CCMapping(15, 0, "Master", 0, 127, "linear", "master"),
|
|
},
|
|
"note_mappings": {
|
|
"scene_1": NoteMapping(32, 0, "Scene 1", "momentary"),
|
|
"scene_2": NoteMapping(33, 0, "Scene 2", "momentary"),
|
|
"scene_3": NoteMapping(34, 0, "Scene 3", "momentary"),
|
|
"scene_4": NoteMapping(35, 0, "Scene 4", "momentary"),
|
|
"panic_button": NoteMapping(36, 0, "Panic", "momentary"),
|
|
"fill_trigger": NoteMapping(37, 0, "Fill Trigger", "momentary"),
|
|
"backup_track": NoteMapping(38, 0, "Backup Track", "toggle"),
|
|
"performance_mode": NoteMapping(39, 0, "Performance Mode", "toggle"),
|
|
"pad_1": NoteMapping(40, 1, "Pad 1", "momentary", feedback_enabled=True),
|
|
"pad_2": NoteMapping(41, 1, "Pad 2", "momentary", feedback_enabled=True),
|
|
"pad_3": NoteMapping(42, 1, "Pad 3", "momentary", feedback_enabled=True),
|
|
"pad_4": NoteMapping(43, 1, "Pad 4", "momentary", feedback_enabled=True),
|
|
"pad_5": NoteMapping(44, 1, "Pad 5", "momentary", feedback_enabled=True),
|
|
"pad_6": NoteMapping(45, 1, "Pad 6", "momentary", feedback_enabled=True),
|
|
"pad_7": NoteMapping(46, 1, "Pad 7", "momentary", feedback_enabled=True),
|
|
"pad_8": NoteMapping(47, 1, "Pad 8", "momentary", feedback_enabled=True),
|
|
}
|
|
}
|
|
|
|
# Mapeos predefinidos para AKAI APC40 MKII
|
|
AKAI_APC40_MK2_MAPPINGS = {
|
|
"cc_mappings": {
|
|
"master_fader": CCMapping(14, 0, "Master Fader", 0, 127, "linear", "master"),
|
|
"fader_1": CCMapping(48, 0, "Fader 1", 0, 127, "linear", "drums_bus"),
|
|
"fader_2": CCMapping(49, 0, "Fader 2", 0, 127, "linear", "bass_bus"),
|
|
"fader_3": CCMapping(50, 0, "Fader 3", 0, 127, "linear", "music_bus"),
|
|
"fader_4": CCMapping(51, 0, "Fader 4", 0, 127, "linear", "music_bus"),
|
|
"knob_1": CCMapping(16, 0, "Knob 1", 0, 127, "linear", "drums_bus"),
|
|
"knob_2": CCMapping(17, 0, "Knob 2", 0, 127, "linear", "bass_bus"),
|
|
"knob_3": CCMapping(18, 0, "Knob 3", 0, 127, "linear", "music_bus"),
|
|
"knob_4": CCMapping(19, 0, "Knob 4", 0, 127, "linear", "master"),
|
|
"knob_5": CCMapping(20, 0, "Knob 5", 0, 127, "exponential", None), # Humanize
|
|
"knob_6": CCMapping(21, 0, "Knob 6", 0, 127, "exponential", None), # Sidechain
|
|
"knob_7": CCMapping(22, 0, "Knob 7", 0, 127, "exponential", None), # Reverb
|
|
"knob_8": CCMapping(23, 0, "Knob 8", 0, 127, "exponential", None), # Delay
|
|
},
|
|
"note_mappings": {
|
|
"clip_launch_1": NoteMapping(53, 0, "Clip 1", "momentary", feedback_enabled=True, led_color=1),
|
|
"clip_launch_2": NoteMapping(54, 0, "Clip 2", "momentary", feedback_enabled=True, led_color=1),
|
|
"clip_launch_3": NoteMapping(55, 0, "Clip 3", "momentary", feedback_enabled=True, led_color=1),
|
|
"clip_launch_4": NoteMapping(56, 0, "Clip 4", "momentary", feedback_enabled=True, led_color=1),
|
|
"scene_launch_1": NoteMapping(82, 0, "Scene 1", "momentary", feedback_enabled=True, led_color=2),
|
|
"scene_launch_2": NoteMapping(83, 0, "Scene 2", "momentary", feedback_enabled=True, led_color=2),
|
|
"scene_launch_3": NoteMapping(84, 0, "Scene 3", "momentary", feedback_enabled=True, led_color=2),
|
|
"scene_launch_4": NoteMapping(85, 0, "Scene 4", "momentary", feedback_enabled=True, led_color=2),
|
|
"panic": NoteMapping(87, 0, "Panic", "momentary", feedback_enabled=True, led_color=2),
|
|
"shift": NoteMapping(98, 0, "Shift", "hold"),
|
|
"bank_select": NoteMapping(99, 0, "Bank", "toggle"),
|
|
}
|
|
}
|
|
|
|
# Mapeos para Pioneer DDJ (mapeo MIDI estándar)
|
|
PIONEER_DDJ_MAPPINGS = {
|
|
"cc_mappings": {
|
|
"channel_1_fader": CCMapping(2, 0, "CH1 Fader", 0, 127, "linear", "drums_bus"),
|
|
"channel_2_fader": CCMapping(3, 0, "CH2 Fader", 0, 127, "linear", "bass_bus"),
|
|
"channel_3_fader": CCMapping(4, 0, "CH3 Fader", 0, 127, "linear", "music_bus"),
|
|
"channel_4_fader": CCMapping(5, 0, "CH4 Fader", 0, 127, "linear", "music_bus"),
|
|
"master_fader": CCMapping(6, 0, "Master", 0, 127, "linear", "master"),
|
|
"crossfader": CCMapping(8, 0, "Crossfader", 0, 127, "linear"),
|
|
"eq_high_1": CCMapping(10, 0, "EQ High 1", 0, 127, "linear", "drums_bus"),
|
|
"eq_mid_1": CCMapping(11, 0, "EQ Mid 1", 0, 127, "linear", "drums_bus"),
|
|
"eq_low_1": CCMapping(12, 0, "EQ Low 1", 0, 127, "linear", "drums_bus"),
|
|
"eq_high_2": CCMapping(13, 0, "EQ High 2", 0, 127, "linear", "bass_bus"),
|
|
"eq_mid_2": CCMapping(14, 0, "EQ Mid 2", 0, 127, "linear", "bass_bus"),
|
|
"eq_low_2": CCMapping(15, 0, "EQ Low 2", 0, 127, "linear", "bass_bus"),
|
|
"filter_1": CCMapping(20, 0, "Filter 1", 0, 127, "linear", "drums_bus"),
|
|
"filter_2": CCMapping(21, 0, "Filter 2", 0, 127, "linear", "bass_bus"),
|
|
"gain_1": CCMapping(30, 0, "Gain 1", 0, 127, "exponential"),
|
|
"gain_2": CCMapping(31, 0, "Gain 2", 0, 127, "exponential"),
|
|
},
|
|
"note_mappings": {
|
|
"play_1": NoteMapping(11, 0, "Play 1", "toggle", feedback_enabled=True),
|
|
"play_2": NoteMapping(12, 0, "Play 2", "toggle", feedback_enabled=True),
|
|
"cue_1": NoteMapping(9, 0, "Cue 1", "momentary", feedback_enabled=True),
|
|
"cue_2": NoteMapping(10, 0, "Cue 2", "momentary", feedback_enabled=True),
|
|
"sync_1": NoteMapping(13, 0, "Sync 1", "toggle", feedback_enabled=True),
|
|
"sync_2": NoteMapping(14, 0, "Sync 2", "toggle", feedback_enabled=True),
|
|
"hotcue_1": NoteMapping(20, 0, "Hotcue 1", "momentary", feedback_enabled=True),
|
|
"hotcue_2": NoteMapping(21, 0, "Hotcue 2", "momentary", feedback_enabled=True),
|
|
"hotcue_3": NoteMapping(22, 0, "Hotcue 3", "momentary", feedback_enabled=True),
|
|
"hotcue_4": NoteMapping(23, 0, "Hotcue 4", "momentary", feedback_enabled=True),
|
|
}
|
|
}
|
|
|
|
|
|
def get_hardware_mapping(hardware_type: str) -> Dict[str, Any]:
|
|
"""
|
|
T166: Obtiene mapeo MIDI completo para controladores soportados.
|
|
|
|
Args:
|
|
hardware_type: Tipo de hardware ('xone_k2', 'akai_apc40', 'pioneer_ddj', etc.)
|
|
|
|
Returns:
|
|
Dict con configuración completa de mapeo CC y Note.
|
|
|
|
Ejemplos:
|
|
>>> get_hardware_mapping('xone_k2')
|
|
>>> get_hardware_mapping('akai_apc40')
|
|
"""
|
|
hardware_type = hardware_type.lower().replace(" ", "_")
|
|
|
|
config_map = {
|
|
"xone_k2": XONE_K2_MAPPINGS,
|
|
"xone:k2": XONE_K2_MAPPINGS,
|
|
"akai_apc40": AKAI_APC40_MK2_MAPPINGS,
|
|
"apc40": AKAI_APC40_MK2_MAPPINGS,
|
|
"apc40_mk2": AKAI_APC40_MK2_MAPPINGS,
|
|
"apc40_mkii": AKAI_APC40_MK2_MAPPINGS,
|
|
"pioneer_ddj": PIONEER_DDJ_MAPPINGS,
|
|
"ddj": PIONEER_DDJ_MAPPINGS,
|
|
}
|
|
|
|
if hardware_type not in config_map:
|
|
logger.warning(f"[HARDWARE] Tipo no reconocido: {hardware_type}, usando mapeo genérico")
|
|
return XONE_K2_MAPPINGS # Default fallback
|
|
|
|
mapping = config_map[hardware_type]
|
|
logger.info(f"[HARDWARE] T166: Mapeo cargado para {hardware_type}")
|
|
|
|
return {
|
|
"hardware_type": hardware_type,
|
|
"cc_count": len(mapping["cc_mappings"]),
|
|
"note_count": len(mapping["note_mappings"]),
|
|
"mappings": mapping,
|
|
"status": "mapped"
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# T167: Callbacks Asíncronos para Filtros de Hardware
|
|
# =============================================================================
|
|
|
|
class AsyncFilterController:
|
|
"""
|
|
T167: Controlador asíncrono para filtros de hardware.
|
|
|
|
Liga CC de filtros de hardware a buses de forma asíncrona,
|
|
permitiendo actualizaciones en tiempo real sin bloquear.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.active_filters: Dict[str, Dict[str, Any]] = {}
|
|
self._lock = asyncio.Lock()
|
|
self._callbacks: Dict[str, List[Callable]] = {}
|
|
|
|
async def register_filter_callback(
|
|
self,
|
|
filter_name: str,
|
|
bus_name: str,
|
|
callback: Callable[[float], Any]
|
|
) -> bool:
|
|
"""Registra un callback asíncrono para un filtro."""
|
|
async with self._lock:
|
|
self.active_filters[filter_name] = {
|
|
"bus": bus_name,
|
|
"current_value": 0.5,
|
|
"target_value": 0.5,
|
|
"smoothing": 0.1,
|
|
"callback": callback
|
|
}
|
|
logger.info(f"[HARDWARE] T167: Filtro {filter_name} ligado a bus {bus_name}")
|
|
return True
|
|
|
|
async def update_filter_value(self, filter_name: str, cc_value: int) -> bool:
|
|
"""Actualiza valor de filtro desde mensaje CC (0-127)."""
|
|
async with self._lock:
|
|
if filter_name not in self.active_filters:
|
|
return False
|
|
|
|
# Normalizar 0-127 a 0.0-1.0
|
|
normalized = cc_value / 127.0
|
|
self.active_filters[filter_name]["target_value"] = normalized
|
|
|
|
# Aplicar smoothing asíncrono
|
|
await self._apply_smoothing(filter_name)
|
|
|
|
return True
|
|
|
|
async def _apply_smoothing(self, filter_name: str):
|
|
"""Aplica smoothing asíncrono al valor del filtro."""
|
|
filter_data = self.active_filters[filter_name]
|
|
current = filter_data["current_value"]
|
|
target = filter_data["target_value"]
|
|
smoothing = filter_data["smoothing"]
|
|
|
|
# Interpolación exponencial
|
|
new_value = current + (target - current) * smoothing
|
|
filter_data["current_value"] = new_value
|
|
|
|
# Ejecutar callback si existe
|
|
if filter_data["callback"]:
|
|
try:
|
|
if asyncio.iscoroutinefunction(filter_data["callback"]):
|
|
await filter_data["callback"](new_value)
|
|
else:
|
|
filter_data["callback"](new_value)
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] Error en callback de filtro: {e}")
|
|
|
|
async def get_filter_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado de todos los filtros activos."""
|
|
async with self._lock:
|
|
return {
|
|
name: {
|
|
"bus": data["bus"],
|
|
"value": data["current_value"],
|
|
"target": data["target_value"]
|
|
}
|
|
for name, data in self.active_filters.items()
|
|
}
|
|
|
|
|
|
# Instancia global del controlador de filtros
|
|
_filter_controller = AsyncFilterController()
|
|
|
|
|
|
async def bind_filter_to_bus_async(
|
|
filter_cc: int,
|
|
bus_name: str,
|
|
hardware_type: str = "xone_k2"
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
T167: Liga un CC de filtro de hardware a un bus asíncronamente.
|
|
|
|
Args:
|
|
filter_cc: Número de CC del filtro
|
|
bus_name: Nombre del bus objetivo (drums_bus, bass_bus, music_bus, master)
|
|
hardware_type: Tipo de controlador
|
|
|
|
Returns:
|
|
Dict con estado de la ligadura.
|
|
"""
|
|
filter_name = f"filter_{filter_cc}"
|
|
|
|
# Callback que actualizará el filtro en Live
|
|
async def filter_callback(value: float):
|
|
# Aquí se conectaría con el MCP server para actualizar el filtro
|
|
logger.debug(f"[HARDWARE] T167: Filtro {filter_name} = {value:.3f}")
|
|
|
|
await _filter_controller.register_filter_callback(filter_name, bus_name, filter_callback)
|
|
|
|
return {
|
|
"filter_cc": filter_cc,
|
|
"bus_name": bus_name,
|
|
"status": "async_bound",
|
|
"smoothing": 0.1,
|
|
"message": f"Filtro CC{filter_cc} ligado asíncronamente a {bus_name}"
|
|
}
|
|
|
|
|
|
# =============================================================================
|
|
# T168: Monitor de Pista vía Hardware
|
|
# =============================================================================
|
|
|
|
class TrackMonitorController:
|
|
"""T168: Controla monitor de pista desde hardware."""
|
|
|
|
def __init__(self):
|
|
self.monitor_states: Dict[int, bool] = {} # track_index -> bool
|
|
self._lock = threading.Lock()
|
|
|
|
def toggle_monitor(self, track_index: int) -> Dict[str, Any]:
|
|
"""Activa/desactiva monitor de pista específica."""
|
|
with self._lock:
|
|
current_state = self.monitor_states.get(track_index, False)
|
|
new_state = not current_state
|
|
self.monitor_states[track_index] = new_state
|
|
|
|
logger.info(f"[HARDWARE] T168: Monitor de pista {track_index} = {new_state}")
|
|
|
|
return {
|
|
"track_index": track_index,
|
|
"monitor_active": new_state,
|
|
"action": "toggle_monitor"
|
|
}
|
|
|
|
def set_monitor(self, track_index: int, state: bool) -> Dict[str, Any]:
|
|
"""Establece estado de monitor explícitamente."""
|
|
with self._lock:
|
|
self.monitor_states[track_index] = state
|
|
|
|
return {
|
|
"track_index": track_index,
|
|
"monitor_active": state,
|
|
"action": "set_monitor"
|
|
}
|
|
|
|
|
|
_track_monitor = TrackMonitorController()
|
|
|
|
|
|
def toggle_track_monitor(track_index: int) -> Dict[str, Any]:
|
|
"""T168: Activa/desactiva monitor de pista desde hardware."""
|
|
return _track_monitor.toggle_monitor(track_index)
|
|
|
|
|
|
# =============================================================================
|
|
# T169: MIDI Clock Externo y Tempo Dinámico
|
|
# =============================================================================
|
|
|
|
class MIDIClockSync:
|
|
"""
|
|
T169: Sincronización de MIDI Clock externos.
|
|
|
|
Recibe pulsos de clock MIDI y ajusta el tempo del set dinámicamente.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.is_running = False
|
|
self.clock_pulses = deque(maxlen=24) # 24 ppqn
|
|
self.last_pulse_time = 0.0
|
|
self.current_bpm = 120.0
|
|
self._target_bpm = 120.0
|
|
self._smoothing = 0.3
|
|
self._callback: Optional[Callable[[float], None]] = None
|
|
|
|
def start(self, callback: Optional[Callable[[float], None]] = None):
|
|
"""Inicia recepción de MIDI Clock."""
|
|
self.is_running = True
|
|
self._callback = callback
|
|
logger.info("[HARDWARE] T169: MIDI Clock sync iniciado")
|
|
|
|
def stop(self):
|
|
"""Detiene recepción de MIDI Clock."""
|
|
self.is_running = False
|
|
logger.info("[HARDWARE] T169: MIDI Clock sync detenido")
|
|
|
|
def receive_clock_pulse(self):
|
|
"""Procesa un pulso de clock MIDI (llamado 24 veces por negra)."""
|
|
if not self.is_running:
|
|
return
|
|
|
|
current_time = time.time()
|
|
|
|
if self.last_pulse_time > 0:
|
|
pulse_interval = current_time - self.last_pulse_time
|
|
|
|
# Calcular BPM instantáneo (24 pulsos = 1 negra)
|
|
if pulse_interval > 0:
|
|
instant_bpm = 60.0 / (pulse_interval * 24.0)
|
|
|
|
# Limitar a rango válido
|
|
instant_bpm = max(60.0, min(200.0, instant_bpm))
|
|
|
|
# Smoothing
|
|
self._target_bpm = instant_bpm
|
|
self.current_bpm += (self._target_bpm - self.current_bpm) * self._smoothing
|
|
|
|
# Callback para actualizar Live
|
|
if self._callback:
|
|
self._callback(self.current_bpm)
|
|
|
|
self.last_pulse_time = current_time
|
|
self.clock_pulses.append(current_time)
|
|
|
|
def receive_start(self):
|
|
"""Procesa mensaje MIDI Start."""
|
|
self.clock_pulses.clear()
|
|
self.last_pulse_time = 0.0
|
|
logger.info("[HARDWARE] T169: MIDI Start recibido")
|
|
|
|
def receive_stop(self):
|
|
"""Procesa mensaje MIDI Stop."""
|
|
logger.info("[HARDWARE] T169: MIDI Stop recibido")
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado actual de sincronización."""
|
|
return {
|
|
"running": self.is_running,
|
|
"current_bpm": round(self.current_bpm, 2),
|
|
"target_bpm": round(self._target_bpm, 2),
|
|
"pulse_count": len(self.clock_pulses)
|
|
}
|
|
|
|
|
|
_midi_clock = MIDIClockSync()
|
|
|
|
|
|
def set_midi_clock_callback(callback: Callable[[float], None]):
|
|
"""Establece callback para cambios de tempo por MIDI Clock."""
|
|
_midi_clock._callback = callback
|
|
|
|
|
|
def start_midi_clock_sync() -> Dict[str, Any]:
|
|
"""T169: Inicia sincronización con MIDI Clock externo."""
|
|
_midi_clock.start()
|
|
return {
|
|
"status": "started",
|
|
"message": "Sincronización MIDI Clock iniciada",
|
|
"ppqn": 24
|
|
}
|
|
|
|
|
|
def stop_midi_clock_sync() -> Dict[str, Any]:
|
|
"""Detiene sincronización MIDI Clock."""
|
|
_midi_clock.stop()
|
|
return {
|
|
"status": "stopped",
|
|
"message": "Sincronización MIDI Clock detenida"
|
|
}
|
|
|
|
|
|
def get_midi_clock_status() -> Dict[str, Any]:
|
|
"""Obtiene estado de sincronización MIDI Clock."""
|
|
return _midi_clock.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T170: Gain Staging Mapeado a Fader Master
|
|
# =============================================================================
|
|
|
|
class GainStagingController:
|
|
"""
|
|
T170: Controla gain staging desde fader master del controlador.
|
|
|
|
Mapea el fader master a calibración de gain staging del set.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.current_value = 0.85 # Valor por defecto (0dB)
|
|
self.target_lufs = -14.0
|
|
self._lock = threading.Lock()
|
|
|
|
def update_from_fader(self, cc_value: int) -> Dict[str, Any]:
|
|
"""
|
|
Actualiza gain staging desde valor CC del fader (0-127).
|
|
|
|
Mapeo:
|
|
- 0-63: LUFS más bajo (-23 a -14)
|
|
- 64-127: LUFS más alto (-14 a -8)
|
|
"""
|
|
with self._lock:
|
|
normalized = cc_value / 127.0
|
|
|
|
# Mapear a rango de LUFS
|
|
if normalized < 0.5:
|
|
# Streaming range
|
|
self.target_lufs = -23.0 + (normalized * 2 * 9.0) # -23 a -14
|
|
else:
|
|
# Club range
|
|
self.target_lufs = -14.0 + ((normalized - 0.5) * 2 * 6.0) # -14 a -8
|
|
|
|
self.current_value = normalized
|
|
|
|
logger.info(f"[HARDWARE] T170: Gain staging ajustado a {self.target_lufs:.1f} LUFS")
|
|
|
|
return {
|
|
"cc_value": cc_value,
|
|
"normalized": round(normalized, 3),
|
|
"target_lufs": round(self.target_lufs, 1),
|
|
"action": "gain_staging_update"
|
|
}
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado actual de gain staging."""
|
|
return {
|
|
"current_value": round(self.current_value, 3),
|
|
"target_lufs": round(self.target_lufs, 1),
|
|
"range": "streaming" if self.target_lufs <= -14 else "club"
|
|
}
|
|
|
|
|
|
_gain_staging = GainStagingController()
|
|
|
|
|
|
def update_gain_staging_from_fader(cc_value: int) -> Dict[str, Any]:
|
|
"""
|
|
T170: Actualiza calibración de gain staging desde fader master.
|
|
|
|
Args:
|
|
cc_value: Valor CC del fader (0-127)
|
|
|
|
Returns:
|
|
Dict con estado actualizado de gain staging.
|
|
"""
|
|
return _gain_staging.update_from_fader(cc_value)
|
|
|
|
|
|
def get_gain_staging_status() -> Dict[str, Any]:
|
|
"""Obtiene estado actual de gain staging."""
|
|
return _gain_staging.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T171: Disparo de Fills desde Pads de Drum Rack
|
|
# =============================================================================
|
|
|
|
class DrumPadController:
|
|
"""
|
|
T171: Controla disparo de fills desde pads del Drum Rack.
|
|
|
|
Mapea pads físicos del controlador a fills de patrón.
|
|
"""
|
|
|
|
FILL_PATTERNS = {
|
|
"fill_1": {"density": "sparse", "section": "drop"},
|
|
"fill_2": {"density": "medium", "section": "build"},
|
|
"fill_3": {"density": "heavy", "section": "drop"},
|
|
"fill_4": {"density": "sparse", "section": "break"},
|
|
}
|
|
|
|
def __init__(self):
|
|
self.active_fills: Dict[str, bool] = {}
|
|
self._callback: Optional[Callable[[str, str, str], None]] = None
|
|
|
|
def register_fill_callback(self, callback: Callable[[str, str, str], None]):
|
|
"""Registra callback para disparo de fills."""
|
|
self._callback = callback
|
|
|
|
def trigger_fill(self, pad_number: int) -> Dict[str, Any]:
|
|
"""
|
|
Dispara un fill desde un pad específico.
|
|
|
|
Args:
|
|
pad_number: Número de pad (1-8)
|
|
"""
|
|
fill_name = f"fill_{pad_number}"
|
|
|
|
if fill_name not in self.FILL_PATTERNS:
|
|
return {
|
|
"pad": pad_number,
|
|
"status": "error",
|
|
"message": f"Pad {pad_number} no mapeado"
|
|
}
|
|
|
|
pattern = self.FILL_PATTERNS[fill_name]
|
|
self.active_fills[fill_name] = True
|
|
|
|
# Ejecutar callback si existe
|
|
if self._callback:
|
|
try:
|
|
self._callback(fill_name, pattern["density"], pattern["section"])
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T171: Error en callback de fill: {e}")
|
|
|
|
logger.info(f"[HARDWARE] T171: Fill disparado desde pad {pad_number} ({pattern['density']})")
|
|
|
|
return {
|
|
"pad": pad_number,
|
|
"fill_name": fill_name,
|
|
"density": pattern["density"],
|
|
"section": pattern["section"],
|
|
"status": "triggered"
|
|
}
|
|
|
|
def get_pad_mappings(self) -> Dict[str, Any]:
|
|
"""Retorna mapeo de todos los pads."""
|
|
return {
|
|
"pads": {
|
|
str(i): self.FILL_PATTERNS.get(f"fill_{i}", {"density": "none"})
|
|
for i in range(1, 9)
|
|
}
|
|
}
|
|
|
|
|
|
_drum_pad_controller = DrumPadController()
|
|
|
|
|
|
def trigger_fill_from_pad(pad_number: int) -> Dict[str, Any]:
|
|
"""
|
|
T171: Dispara fill de patrón desde pad del Drum Rack.
|
|
|
|
Args:
|
|
pad_number: Número de pad (1-8)
|
|
|
|
Returns:
|
|
Dict con información del fill disparado.
|
|
"""
|
|
return _drum_pad_controller.trigger_fill(pad_number)
|
|
|
|
|
|
def register_fill_callback(callback: Callable[[str, str, str], None]):
|
|
"""Registra callback para fills desde pads."""
|
|
_drum_pad_controller.register_fill_callback(callback)
|
|
|
|
|
|
# =============================================================================
|
|
# T172: Botón de Pánico (apaga delays y reverbs)
|
|
# =============================================================================
|
|
|
|
class PanicButtonController:
|
|
"""
|
|
T172: Botón de pánico para emergencias en vivo.
|
|
|
|
Apaga inmediatamente delays, reverbs y efectos de cola.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.is_active = False
|
|
self._callback: Optional[Callable[[], None]] = None
|
|
self.affected_tracks: List[str] = ["music_bus", "vocal_bus", "atmos_bus"]
|
|
|
|
def register_callback(self, callback: Callable[[], None]):
|
|
"""Registra callback para activación de pánico."""
|
|
self._callback = callback
|
|
|
|
def trigger_panic(self) -> Dict[str, Any]:
|
|
"""
|
|
Activa modo pánico: apaga todos los efectos de cola.
|
|
|
|
Esto incluye:
|
|
- Reverbs (envíos a buses de retorno)
|
|
- Delays (taps de delay)
|
|
- Efectos con cola larga
|
|
"""
|
|
self.is_active = True
|
|
|
|
# Ejecutar callback si existe
|
|
if self._callback:
|
|
try:
|
|
self._callback()
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T172: Error en callback de pánico: {e}")
|
|
|
|
logger.warning("[HARDWARE] T172: BOTÓN DE PÁNICO ACTIVADO - Efectos detenidos")
|
|
|
|
return {
|
|
"status": "PANIC_ACTIVATED",
|
|
"message": "Delays y reverbs detenidos",
|
|
"affected_tracks": self.affected_tracks,
|
|
"timestamp": time.time()
|
|
}
|
|
|
|
def release_panic(self) -> Dict[str, Any]:
|
|
"""Libera modo pánico, restaura efectos gradualmente."""
|
|
self.is_active = False
|
|
|
|
logger.info("[HARDWARE] T172: Modo pánico liberado")
|
|
|
|
return {
|
|
"status": "released",
|
|
"message": "Efectos restaurados gradualmente"
|
|
}
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado del botón de pánico."""
|
|
return {
|
|
"active": self.is_active,
|
|
"affected_tracks": self.affected_tracks
|
|
}
|
|
|
|
|
|
_panic_controller = PanicButtonController()
|
|
|
|
|
|
def trigger_panic_button() -> Dict[str, Any]:
|
|
"""
|
|
T172: Activa botón de pánico desde hardware.
|
|
|
|
Apaga inmediatamente todos los delays y reverbs del set.
|
|
|
|
Returns:
|
|
Dict con estado de activación.
|
|
"""
|
|
return _panic_controller.trigger_panic()
|
|
|
|
|
|
def release_panic_button() -> Dict[str, Any]:
|
|
"""Libera modo pánico."""
|
|
return _panic_controller.release_panic()
|
|
|
|
|
|
def register_panic_callback(callback: Callable[[], None]):
|
|
"""Registra callback para botón de pánico."""
|
|
_panic_controller.register_callback(callback)
|
|
|
|
|
|
# =============================================================================
|
|
# T173: Feedback Luminoso al Hardware
|
|
# =============================================================================
|
|
|
|
class HardwareFeedbackController:
|
|
"""
|
|
T173: Controla feedback luminoso hacia el hardware.
|
|
|
|
Envia mensajes MIDI de vuelta al controlador para:
|
|
- LEDs de pads
|
|
- Anillos LED de knobs
|
|
- Displays
|
|
"""
|
|
|
|
LED_COLORS_APC40 = {
|
|
"off": 0,
|
|
"green": 1,
|
|
"green_blink": 2,
|
|
"red": 3,
|
|
"red_blink": 4,
|
|
"yellow": 5,
|
|
"yellow_blink": 6,
|
|
"orange": 7,
|
|
}
|
|
|
|
def __init__(self):
|
|
self.output_port: Optional[Any] = None
|
|
self._lock = threading.Lock()
|
|
self.active_leds: Dict[str, int] = {}
|
|
|
|
def set_output_port(self, port: Any):
|
|
"""Establece puerto MIDI de salida."""
|
|
self.output_port = port
|
|
|
|
def send_pad_led(self, note: int, color: str, channel: int = 0) -> bool:
|
|
"""
|
|
Envía mensaje de LED a un pad.
|
|
|
|
Args:
|
|
note: Número de nota del pad
|
|
color: Nombre del color (ver LED_COLORS_APC40)
|
|
channel: Canal MIDI
|
|
"""
|
|
if not MIDO_AVAILABLE or not self.output_port:
|
|
return False
|
|
|
|
try:
|
|
color_value = self.LED_COLORS_APC40.get(color, 0)
|
|
msg = Message('note_on', note=note, velocity=color_value, channel=channel)
|
|
self.output_port.send(msg)
|
|
|
|
with self._lock:
|
|
self.active_leds[f"pad_{note}"] = color_value
|
|
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T173: Error enviando LED: {e}")
|
|
return False
|
|
|
|
def send_ring_led(self, cc: int, value: int, channel: int = 0) -> bool:
|
|
"""Envía valor a anillo LED de knob."""
|
|
if not MIDO_AVAILABLE or not self.output_port:
|
|
return False
|
|
|
|
try:
|
|
msg = Message('control_change', control=cc, value=value, channel=channel)
|
|
self.output_port.send(msg)
|
|
return True
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T173: Error enviando ring LED: {e}")
|
|
return False
|
|
|
|
def blink_pattern(self, note: int, color: str, duration_ms: int = 500) -> bool:
|
|
"""
|
|
Hace parpadear un LED en patrón.
|
|
|
|
Útil para indicar estados como exportación de stems.
|
|
"""
|
|
if not MIDO_AVAILABLE or not self.output_port:
|
|
return False
|
|
|
|
def blink_thread():
|
|
start_time = time.time()
|
|
while (time.time() - start_time) * 1000 < duration_ms:
|
|
self.send_pad_led(note, color)
|
|
time.sleep(0.1)
|
|
self.send_pad_led(note, "off")
|
|
time.sleep(0.1)
|
|
# Estado final
|
|
self.send_pad_led(note, color)
|
|
|
|
threading.Thread(target=blink_thread, daemon=True).start()
|
|
return True
|
|
|
|
def indicate_export_active(self) -> Dict[str, Any]:
|
|
"""
|
|
T173: Indica exportación activa con parpadeo de LEDs.
|
|
|
|
Usa LEDs de escenas para mostrar progreso de exportación.
|
|
"""
|
|
# Parpadear todas las escenas secuencialmente
|
|
scene_notes = [82, 83, 84, 85] # APC40 scene launch buttons
|
|
|
|
def export_indicator():
|
|
for note in scene_notes:
|
|
self.send_pad_led(note, "green_blink")
|
|
time.sleep(0.2)
|
|
# Todas encienden al completar
|
|
for note in scene_notes:
|
|
self.send_pad_led(note, "green")
|
|
time.sleep(1.0)
|
|
# Reset
|
|
for note in scene_notes:
|
|
self.send_pad_led(note, "off")
|
|
|
|
threading.Thread(target=export_indicator, daemon=True).start()
|
|
|
|
logger.info("[HARDWARE] T173: Feedback de exportación activado")
|
|
|
|
return {
|
|
"status": "export_indicated",
|
|
"led_pattern": "sequential_blink",
|
|
"duration_ms": 2000
|
|
}
|
|
|
|
|
|
_feedback_controller = HardwareFeedbackController()
|
|
|
|
|
|
def set_feedback_output_port(port: Any):
|
|
"""Establece puerto de salida para feedback."""
|
|
_feedback_controller.set_output_port(port)
|
|
|
|
|
|
def indicate_export_on_hardware() -> Dict[str, Any]:
|
|
"""
|
|
T173: Activa indicación visual de exportación en hardware.
|
|
|
|
Returns:
|
|
Dict con estado de la indicación.
|
|
"""
|
|
return _feedback_controller.indicate_export_active()
|
|
|
|
|
|
def send_pad_led_feedback(note: int, color: str, channel: int = 0) -> bool:
|
|
"""Envía feedback de LED a pad específico."""
|
|
return _feedback_controller.send_pad_led(note, color, channel)
|
|
|
|
|
|
# =============================================================================
|
|
# T174: CPU Load en Display/LED Ring
|
|
# =============================================================================
|
|
|
|
class CPUUsageMonitor:
|
|
"""
|
|
T174: Monitorea CPU y envía a display/LED ring del hardware.
|
|
|
|
Detecta carga de CPU desde Live y la muestra en:
|
|
- Displays de 7 segmentos
|
|
- LED rings de knobs
|
|
- LEDs de nivel
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.current_load = 0.0
|
|
self._monitoring = False
|
|
self._thread: Optional[threading.Thread] = None
|
|
|
|
def start_monitoring(self, interval_ms: int = 500):
|
|
"""Inicia monitoreo de CPU."""
|
|
if self._monitoring:
|
|
return
|
|
|
|
self._monitoring = True
|
|
|
|
def monitor_loop():
|
|
while self._monitoring:
|
|
# Simular lectura de CPU de Live
|
|
# En implementación real, esto consultaría Live API
|
|
self.current_load = self._get_live_cpu_load()
|
|
|
|
# Enviar a hardware
|
|
self._send_to_hardware(self.current_load)
|
|
|
|
time.sleep(interval_ms / 1000.0)
|
|
|
|
self._thread = threading.Thread(target=monitor_loop, daemon=True)
|
|
self._thread.start()
|
|
|
|
logger.info("[HARDWARE] T174: Monitoreo de CPU iniciado")
|
|
|
|
def stop_monitoring(self):
|
|
"""Detiene monitoreo de CPU."""
|
|
self._monitoring = False
|
|
|
|
def _get_live_cpu_load(self) -> float:
|
|
"""Obtiene carga de CPU desde Live (simulado)."""
|
|
# En implementación real, consultaría Live API
|
|
# Por ahora, valor simulado
|
|
import random
|
|
return random.uniform(20.0, 60.0)
|
|
|
|
def _send_to_hardware(self, load: float):
|
|
"""Envía valor de CPU a display/LED ring."""
|
|
# Mapear 0-100% a valor MIDI 0-127
|
|
midi_value = int((load / 100.0) * 127)
|
|
|
|
# Enviar a ring LED del knob master (CC 14 en APC40)
|
|
if MIDO_AVAILABLE and _feedback_controller.output_port:
|
|
try:
|
|
msg = Message('control_change', control=14, value=midi_value)
|
|
_feedback_controller.output_port.send(msg)
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T174: Error enviando CPU load: {e}")
|
|
|
|
def get_current_load(self) -> Dict[str, Any]:
|
|
"""Retorna carga actual de CPU."""
|
|
return {
|
|
"cpu_load_percent": round(self.current_load, 1),
|
|
"monitoring": self._monitoring,
|
|
"midi_value": int((self.current_load / 100.0) * 127)
|
|
}
|
|
|
|
|
|
_cpu_monitor = CPUUsageMonitor()
|
|
|
|
|
|
def start_cpu_monitoring(interval_ms: int = 500) -> Dict[str, Any]:
|
|
"""
|
|
T174: Inicia monitoreo de CPU en display/LED ring.
|
|
|
|
Args:
|
|
interval_ms: Intervalo de actualización en milisegundos
|
|
|
|
Returns:
|
|
Dict con estado del monitoreo.
|
|
"""
|
|
_cpu_monitor.start_monitoring(interval_ms)
|
|
return {
|
|
"status": "monitoring_started",
|
|
"interval_ms": interval_ms,
|
|
"display_target": "led_ring"
|
|
}
|
|
|
|
|
|
def stop_cpu_monitoring() -> Dict[str, Any]:
|
|
"""Detiene monitoreo de CPU."""
|
|
_cpu_monitor.stop_monitoring()
|
|
return {
|
|
"status": "monitoring_stopped"
|
|
}
|
|
|
|
|
|
def get_cpu_load() -> Dict[str, Any]:
|
|
"""Obtiene carga actual de CPU."""
|
|
return _cpu_monitor.get_current_load()
|
|
|
|
|
|
# =============================================================================
|
|
# T175: Disparo de Scene desde Controlador
|
|
# =============================================================================
|
|
|
|
class SceneTriggerController:
|
|
"""
|
|
T175: Controla disparo de scenes desde controlador hardware.
|
|
|
|
Incluye cuantización global para sincronización perfecta.
|
|
"""
|
|
|
|
QUANTIZATION_MODES = {
|
|
"none": 0, # Inmediato
|
|
"8th": 0.5, # Corchea
|
|
"4th": 1.0, # Negra
|
|
"2nd": 2.0, # Blanca
|
|
"1bar": 4.0, # Compás
|
|
"2bar": 8.0, # 2 compases
|
|
}
|
|
|
|
def __init__(self):
|
|
self.quantization = "1bar"
|
|
self._callback: Optional[Callable[[int, str], None]] = None
|
|
|
|
def register_callback(self, callback: Callable[[int, str], None]):
|
|
"""Registra callback para disparo de scene."""
|
|
self._callback = callback
|
|
|
|
def trigger_scene(self, scene_index: int, quantization: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
Dispara una scene específica.
|
|
|
|
Args:
|
|
scene_index: Índice de la scene (0-based)
|
|
quantization: Modo de cuantización (usa global si None)
|
|
"""
|
|
quant = quantization or self.quantization
|
|
quant_beats = self.QUANTIZATION_MODES.get(quant, 4.0)
|
|
|
|
# Ejecutar callback si existe
|
|
if self._callback:
|
|
try:
|
|
self._callback(scene_index, quant)
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T175: Error en callback de scene: {e}")
|
|
|
|
logger.info(f"[HARDWARE] T175: Scene {scene_index} disparada (quant: {quant})")
|
|
|
|
return {
|
|
"scene_index": scene_index,
|
|
"quantization": quant,
|
|
"quantization_beats": quant_beats,
|
|
"status": "triggered"
|
|
}
|
|
|
|
def set_global_quantization(self, mode: str) -> Dict[str, Any]:
|
|
"""Establece cuantización global."""
|
|
if mode not in self.QUANTIZATION_MODES:
|
|
return {
|
|
"status": "error",
|
|
"message": f"Modo no válido. Opciones: {list(self.QUANTIZATION_MODES.keys())}"
|
|
}
|
|
|
|
self.quantization = mode
|
|
|
|
return {
|
|
"quantization": mode,
|
|
"beats": self.QUANTIZATION_MODES[mode],
|
|
"status": "set"
|
|
}
|
|
|
|
def get_quantization_modes(self) -> Dict[str, Any]:
|
|
"""Retorna modos de cuantización disponibles."""
|
|
return {
|
|
"current": self.quantization,
|
|
"available": self.QUANTIZATION_MODES
|
|
}
|
|
|
|
|
|
_scene_controller = SceneTriggerController()
|
|
|
|
|
|
def trigger_scene_from_hardware(scene_index: int, quantization: Optional[str] = None) -> Dict[str, Any]:
|
|
"""
|
|
T175: Dispara scene específica desde controlador.
|
|
|
|
Args:
|
|
scene_index: Índice de la scene (0-based)
|
|
quantization: Modo de cuantización (optional)
|
|
|
|
Returns:
|
|
Dict con información del disparo.
|
|
"""
|
|
return _scene_controller.trigger_scene(scene_index, quantization)
|
|
|
|
|
|
def set_scene_quantization(mode: str) -> Dict[str, Any]:
|
|
"""Establece cuantización global para scenes."""
|
|
return _scene_controller.set_global_quantization(mode)
|
|
|
|
|
|
def register_scene_callback(callback: Callable[[int, str], None]):
|
|
"""Registra callback para disparo de scenes."""
|
|
_scene_controller.register_callback(callback)
|
|
|
|
|
|
# =============================================================================
|
|
# T176: Performance Mode - Faders manejan Stems Automáticos
|
|
# =============================================================================
|
|
|
|
class PerformanceModeController:
|
|
"""
|
|
T176: Modo Performance donde faders controlan stems automáticamente.
|
|
|
|
Asignación dinámica de faders a stems según contexto musical.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.active = False
|
|
self.fader_assignments: Dict[int, Dict[str, Any]] = {}
|
|
self.current_layout = "default"
|
|
|
|
# Layouts predefinidos
|
|
self.LAYOUTS = {
|
|
"default": {
|
|
0: {"name": "Drums", "bus": "drums_bus", "color": "red"},
|
|
1: {"name": "Bass", "bus": "bass_bus", "color": "orange"},
|
|
2: {"name": "Music", "bus": "music_bus", "color": "green"},
|
|
3: {"name": "Master", "bus": "master", "color": "yellow"},
|
|
},
|
|
"dj": {
|
|
0: {"name": "Deck A", "bus": "deck_a", "color": "cyan"},
|
|
1: {"name": "Deck B", "bus": "deck_b", "color": "magenta"},
|
|
2: {"name": "FX", "bus": "fx_bus", "color": "yellow"},
|
|
3: {"name": "Master", "bus": "master", "color": "white"},
|
|
},
|
|
"live": {
|
|
0: {"name": "Kick", "bus": "kick_bus", "color": "red"},
|
|
1: {"name": "Snare", "bus": "snare_bus", "color": "orange"},
|
|
2: {"name": "Synth", "bus": "synth_bus", "color": "green"},
|
|
3: {"name": "Vocals", "bus": "vocal_bus", "color": "blue"},
|
|
}
|
|
}
|
|
|
|
def activate(self, layout: str = "default") -> Dict[str, Any]:
|
|
"""
|
|
Activa modo performance con layout específico.
|
|
|
|
Args:
|
|
layout: Nombre del layout (default, dj, live)
|
|
"""
|
|
if layout not in self.LAYOUTS:
|
|
return {
|
|
"status": "error",
|
|
"message": f"Layout no existe. Opciones: {list(self.LAYOUTS.keys())}"
|
|
}
|
|
|
|
self.active = True
|
|
self.current_layout = layout
|
|
self.fader_assignments = self.LAYOUTS[layout].copy()
|
|
|
|
# Enviar feedback a hardware
|
|
self._send_layout_feedback()
|
|
|
|
logger.info(f"[HARDWARE] T176: Performance Mode activado con layout '{layout}'")
|
|
|
|
return {
|
|
"status": "activated",
|
|
"layout": layout,
|
|
"fader_count": len(self.fader_assignments),
|
|
"assignments": self.fader_assignments
|
|
}
|
|
|
|
def deactivate(self) -> Dict[str, Any]:
|
|
"""Desactiva modo performance."""
|
|
self.active = False
|
|
|
|
logger.info("[HARDWARE] T176: Performance Mode desactivado")
|
|
|
|
return {
|
|
"status": "deactivated"
|
|
}
|
|
|
|
def _send_layout_feedback(self):
|
|
"""Envía feedback visual del layout al hardware."""
|
|
if not _feedback_controller.output_port:
|
|
return
|
|
|
|
# Encender LEDs según layout
|
|
colors = {"red": 3, "orange": 7, "green": 1, "yellow": 5, "cyan": 16, "magenta": 17, "blue": 33, "white": 1}
|
|
|
|
for fader_idx, assignment in self.fader_assignments.items():
|
|
color_name = assignment.get("color", "green")
|
|
color_value = colors.get(color_name, 1)
|
|
|
|
# Mapear a LEDs de escenas
|
|
scene_note = 82 + fader_idx
|
|
_feedback_controller.send_pad_led(scene_note, self._get_color_name(color_value))
|
|
|
|
def _get_color_name(self, value: int) -> str:
|
|
"""Convierte valor de color a nombre."""
|
|
color_map = {0: "off", 1: "green", 3: "red", 5: "yellow", 7: "orange"}
|
|
return color_map.get(value, "green")
|
|
|
|
def handle_fader_move(self, fader_index: int, cc_value: int) -> Dict[str, Any]:
|
|
"""
|
|
Procesa movimiento de fader en modo performance.
|
|
|
|
Args:
|
|
fader_index: Índice del fader (0-based)
|
|
cc_value: Valor CC (0-127)
|
|
"""
|
|
if not self.active:
|
|
return {"status": "inactive", "message": "Performance Mode no está activo"}
|
|
|
|
if fader_index not in self.fader_assignments:
|
|
return {"status": "error", "message": f"Fader {fader_index} no asignado"}
|
|
|
|
assignment = self.fader_assignments[fader_index]
|
|
normalized = cc_value / 127.0
|
|
|
|
return {
|
|
"fader_index": fader_index,
|
|
"assignment": assignment,
|
|
"cc_value": cc_value,
|
|
"normalized": round(normalized, 3),
|
|
"status": "handled"
|
|
}
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado del modo performance."""
|
|
return {
|
|
"active": self.active,
|
|
"layout": self.current_layout,
|
|
"assignments": self.fader_assignments
|
|
}
|
|
|
|
|
|
_performance_controller = PerformanceModeController()
|
|
|
|
|
|
def activate_performance_mode(layout: str = "default") -> Dict[str, Any]:
|
|
"""
|
|
T176: Activa Performance Mode con faders manejando stems.
|
|
|
|
Args:
|
|
layout: Layout de asignación (default, dj, live)
|
|
|
|
Returns:
|
|
Dict con estado del modo performance.
|
|
"""
|
|
return _performance_controller.activate(layout)
|
|
|
|
|
|
def deactivate_performance_mode() -> Dict[str, Any]:
|
|
"""Desactiva modo performance."""
|
|
return _performance_controller.deactivate()
|
|
|
|
|
|
def handle_performance_fader(fader_index: int, cc_value: int) -> Dict[str, Any]:
|
|
"""Procesa movimiento de fader en modo performance."""
|
|
return _performance_controller.handle_fader_move(fader_index, cc_value)
|
|
|
|
|
|
def get_performance_status() -> Dict[str, Any]:
|
|
"""Obtiene estado del modo performance."""
|
|
return _performance_controller.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T177: Humanize como Knob Macro
|
|
# =============================================================================
|
|
|
|
class HumanizeMacroController:
|
|
"""
|
|
T177: Mapea humanize_set como knob macro para control orgánico.
|
|
|
|
Permite incrementar "caos orgánico" gradualmente desde hardware.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.intensity = 0.0
|
|
self._callback: Optional[Callable[[float], None]] = None
|
|
|
|
def register_callback(self, callback: Callable[[float], None]):
|
|
"""Registra callback para cambios de intensidad."""
|
|
self._callback = callback
|
|
|
|
def update_from_knob(self, cc_value: int) -> Dict[str, Any]:
|
|
"""
|
|
Actualiza intensidad de humanización desde knob.
|
|
|
|
Args:
|
|
cc_value: Valor CC del knob (0-127)
|
|
"""
|
|
# Mapear 0-127 a 0.0-1.0
|
|
self.intensity = cc_value / 127.0
|
|
|
|
# Ejecutar callback si existe
|
|
if self._callback:
|
|
try:
|
|
self._callback(self.intensity)
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T177: Error en callback humanize: {e}")
|
|
|
|
# Clasificar nivel
|
|
if self.intensity < 0.3:
|
|
level = "subtle"
|
|
elif self.intensity < 0.6:
|
|
level = "medium"
|
|
else:
|
|
level = "extreme"
|
|
|
|
logger.info(f"[HARDWARE] T177: Humanize = {self.intensity:.2f} ({level})")
|
|
|
|
return {
|
|
"cc_value": cc_value,
|
|
"intensity": round(self.intensity, 3),
|
|
"level": level,
|
|
"status": "updated"
|
|
}
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado actual de humanización."""
|
|
return {
|
|
"intensity": round(self.intensity, 3),
|
|
"cc_range": f"0-127 -> 0.0-1.0"
|
|
}
|
|
|
|
|
|
_humanize_macro = HumanizeMacroController()
|
|
|
|
|
|
def update_humanize_from_knob(cc_value: int) -> Dict[str, Any]:
|
|
"""
|
|
T177: Actualiza humanización desde knob macro.
|
|
|
|
Args:
|
|
cc_value: Valor CC del knob (0-127)
|
|
|
|
Returns:
|
|
Dict con nivel de humanización.
|
|
"""
|
|
return _humanize_macro.update_from_knob(cc_value)
|
|
|
|
|
|
def register_humanize_callback(callback: Callable[[float], None]):
|
|
"""Registra callback para cambios de humanización."""
|
|
_humanize_macro.register_callback(callback)
|
|
|
|
|
|
def get_humanize_macro_status() -> Dict[str, Any]:
|
|
"""Obtiene estado del macro de humanización."""
|
|
return _humanize_macro.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T178: Detección de Silencio y Track de Respaldo
|
|
# =============================================================================
|
|
|
|
class SilenceDetector:
|
|
"""
|
|
T178: Detecta silencio prolongado y auto-lanza track de respaldo.
|
|
|
|
Útil para recuperación de emergencia en vivo.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.silence_threshold_db = -60.0
|
|
self.silence_duration_ms = 3000 # 3 segundos
|
|
self.is_monitoring = False
|
|
self._silence_start: Optional[float] = None
|
|
self._monitor_thread: Optional[threading.Thread] = None
|
|
self._callback: Optional[Callable[[], None]] = None
|
|
|
|
def register_callback(self, callback: Callable[[], None]):
|
|
"""Registra callback para detección de silencio."""
|
|
self._callback = callback
|
|
|
|
def start_monitoring(self, threshold_db: float = -60.0, duration_ms: int = 3000):
|
|
"""
|
|
Inicia monitoreo de silencio.
|
|
|
|
Args:
|
|
threshold_db: Umbral en dB para considerar silencio
|
|
duration_ms: Duración mínima en ms para activar respaldo
|
|
"""
|
|
self.silence_threshold_db = threshold_db
|
|
self.silence_duration_ms = duration_ms
|
|
self.is_monitoring = True
|
|
|
|
def monitor_loop():
|
|
while self.is_monitoring:
|
|
# Simular lectura de nivel desde Live
|
|
current_db = self._get_audio_level()
|
|
|
|
if current_db < self.silence_threshold_db:
|
|
if self._silence_start is None:
|
|
self._silence_start = time.time()
|
|
else:
|
|
elapsed_ms = (time.time() - self._silence_start) * 1000
|
|
if elapsed_ms >= self.silence_duration_ms:
|
|
self._trigger_backup()
|
|
else:
|
|
self._silence_start = None
|
|
|
|
time.sleep(0.1) # Chequeo cada 100ms
|
|
|
|
self._monitor_thread = threading.Thread(target=monitor_loop, daemon=True)
|
|
self._monitor_thread.start()
|
|
|
|
logger.info(f"[HARDWARE] T178: Detección de silencio iniciada ({threshold_db}dB, {duration_ms}ms)")
|
|
|
|
def stop_monitoring(self):
|
|
"""Detiene monitoreo de silencio."""
|
|
self.is_monitoring = False
|
|
|
|
def _get_audio_level(self) -> float:
|
|
"""Obtiene nivel de audio desde Live (simulado)."""
|
|
# En implementación real, consultaría Live API
|
|
import random
|
|
return random.uniform(-30.0, -10.0) # Simulación
|
|
|
|
def _trigger_backup(self):
|
|
"""Activa track de respaldo."""
|
|
logger.warning("[HARDWARE] T178: SILENCIO DETECTADO - Activando track de respaldo")
|
|
|
|
if self._callback:
|
|
try:
|
|
self._callback()
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T178: Error en callback de respaldo: {e}")
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado del detector."""
|
|
return {
|
|
"monitoring": self.is_monitoring,
|
|
"threshold_db": self.silence_threshold_db,
|
|
"duration_ms": self.silence_duration_ms,
|
|
"silence_detected": self._silence_start is not None
|
|
}
|
|
|
|
|
|
_silence_detector = SilenceDetector()
|
|
|
|
|
|
def start_silence_detection(threshold_db: float = -60.0, duration_ms: int = 3000) -> Dict[str, Any]:
|
|
"""
|
|
T178: Inicia detección de silencio prolongado.
|
|
|
|
Args:
|
|
threshold_db: Umbral en dB
|
|
duration_ms: Duración mínima en ms
|
|
|
|
Returns:
|
|
Dict con estado del monitoreo.
|
|
"""
|
|
_silence_detector.start_monitoring(threshold_db, duration_ms)
|
|
return {
|
|
"status": "monitoring_started",
|
|
"threshold_db": threshold_db,
|
|
"duration_ms": duration_ms,
|
|
"action_on_silence": "trigger_backup_track"
|
|
}
|
|
|
|
|
|
def stop_silence_detection() -> Dict[str, Any]:
|
|
"""Detiene detección de silencio."""
|
|
_silence_detector.stop_monitoring()
|
|
return {
|
|
"status": "monitoring_stopped"
|
|
}
|
|
|
|
|
|
def register_silence_callback(callback: Callable[[], None]):
|
|
"""Registra callback para detección de silencio."""
|
|
_silence_detector.register_callback(callback)
|
|
|
|
|
|
def get_silence_detector_status() -> Dict[str, Any]:
|
|
"""Obtiene estado del detector de silencio."""
|
|
return _silence_detector.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T179: Nudging Asíncrono para Corrección de Fase
|
|
# =============================================================================
|
|
|
|
class AsyncNudgeController:
|
|
"""
|
|
T179: Permite nudging asíncrono para corrección de fase.
|
|
|
|
Ajustes micro-temporales sin afectar el tempo global.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.nudge_amount_ms = 0.0
|
|
self._lock = threading.Lock()
|
|
self._callbacks: List[Callable[[float], None]] = []
|
|
|
|
def register_callback(self, callback: Callable[[float], None]):
|
|
"""Registra callback para nudging."""
|
|
self._callbacks.append(callback)
|
|
|
|
def nudge_forward(self, ms: float) -> Dict[str, Any]:
|
|
"""
|
|
Nudge hacia adelante (acelerar momentáneamente).
|
|
|
|
Args:
|
|
ms: Milisegundos de nudge (positivo = adelante)
|
|
"""
|
|
with self._lock:
|
|
self.nudge_amount_ms = ms
|
|
|
|
# Ejecutar callbacks
|
|
for callback in self._callbacks:
|
|
try:
|
|
callback(ms)
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T179: Error en callback de nudge: {e}")
|
|
|
|
logger.info(f"[HARDWARE] T179: Nudge forward {ms}ms")
|
|
|
|
return {
|
|
"direction": "forward",
|
|
"amount_ms": ms,
|
|
"samples_48k": int(ms * 48), # Samples a 48kHz
|
|
"status": "applied"
|
|
}
|
|
|
|
def nudge_backward(self, ms: float) -> Dict[str, Any]:
|
|
"""
|
|
Nudge hacia atrás (atrasar momentáneamente).
|
|
|
|
Args:
|
|
ms: Milisegundos de nudge (positivo = atrás)
|
|
"""
|
|
return self.nudge_forward(-ms)
|
|
|
|
def reset_nudge(self) -> Dict[str, Any]:
|
|
"""Resetea nudge a cero."""
|
|
with self._lock:
|
|
self.nudge_amount_ms = 0.0
|
|
|
|
return {
|
|
"nudge_ms": 0.0,
|
|
"status": "reset"
|
|
}
|
|
|
|
def get_status(self) -> Dict[str, Any]:
|
|
"""Retorna estado de nudging."""
|
|
return {
|
|
"current_nudge_ms": self.nudge_amount_ms,
|
|
"callbacks_registered": len(self._callbacks)
|
|
}
|
|
|
|
|
|
_nudge_controller = AsyncNudgeController()
|
|
|
|
|
|
def apply_nudge_forward(ms: float) -> Dict[str, Any]:
|
|
"""
|
|
T179: Aplica nudging asíncrono hacia adelante.
|
|
|
|
Args:
|
|
ms: Milisegundos de ajuste
|
|
|
|
Returns:
|
|
Dict con información del nudge.
|
|
"""
|
|
return _nudge_controller.nudge_forward(ms)
|
|
|
|
|
|
def apply_nudge_backward(ms: float) -> Dict[str, Any]:
|
|
"""Aplica nudging hacia atrás."""
|
|
return _nudge_controller.nudge_backward(ms)
|
|
|
|
|
|
def reset_nudge() -> Dict[str, Any]:
|
|
"""Resetea nudging a cero."""
|
|
return _nudge_controller.reset_nudge()
|
|
|
|
|
|
def register_nudge_callback(callback: Callable[[float], None]):
|
|
"""Registra callback para nudging."""
|
|
_nudge_controller.register_callback(callback)
|
|
|
|
|
|
def get_nudge_status() -> Dict[str, Any]:
|
|
"""Obtiene estado de nudging."""
|
|
return _nudge_controller.get_status()
|
|
|
|
|
|
# =============================================================================
|
|
# T180: Macros de Visualización
|
|
# =============================================================================
|
|
|
|
class VisualizationMacros:
|
|
"""
|
|
T180: Macros de visualización para la sesión.
|
|
|
|
Incluye indicadores visuales integrados con el hardware.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.macros: Dict[str, Callable[[], None]] = {}
|
|
self._register_default_macros()
|
|
|
|
def _register_default_macros(self):
|
|
"""Registra macros por defecto."""
|
|
self.macros = {
|
|
"strobe_beat": self._strobe_on_beat,
|
|
"level_meter": self._level_meter_display,
|
|
"peak_indicator": self._peak_indicator,
|
|
"recording_active": self._recording_indicator,
|
|
"midi_clock_sync": self._midi_sync_indicator,
|
|
}
|
|
|
|
def _strobe_on_beat(self):
|
|
"""Macro: Strobe sincronizado con beat."""
|
|
if _feedback_controller.output_port:
|
|
# Parpadear todos los pads en rojo
|
|
for i in range(8):
|
|
note = 53 + i
|
|
_feedback_controller.send_pad_led(note, "red")
|
|
time.sleep(0.05)
|
|
_feedback_controller.send_pad_led(note, "off")
|
|
|
|
def _level_meter_display(self):
|
|
"""Macro: Medidor de nivel en LEDs."""
|
|
if _feedback_controller.output_port:
|
|
# Simular medidor
|
|
levels = [20, 40, 60, 80, 100, 80, 60, 40]
|
|
for i, level in enumerate(levels):
|
|
note = 53 + i
|
|
if level > 80:
|
|
color = "red"
|
|
elif level > 50:
|
|
color = "yellow"
|
|
else:
|
|
color = "green"
|
|
_feedback_controller.send_pad_led(note, color)
|
|
|
|
def _peak_indicator(self):
|
|
"""Macro: Indicador de pico."""
|
|
if _feedback_controller.output_port:
|
|
# Parpadear rápido en rojo
|
|
for _ in range(4):
|
|
for i in range(8):
|
|
_feedback_controller.send_pad_led(53 + i, "red")
|
|
time.sleep(0.1)
|
|
for i in range(8):
|
|
_feedback_controller.send_pad_led(53 + i, "off")
|
|
time.sleep(0.1)
|
|
|
|
def _recording_indicator(self):
|
|
"""Macro: Indicador de grabación activa."""
|
|
if _feedback_controller.output_port:
|
|
# LED rojo parpadeante lento
|
|
for _ in range(10):
|
|
_feedback_controller.send_pad_led(82, "red_blink")
|
|
time.sleep(0.5)
|
|
|
|
def _midi_sync_indicator(self):
|
|
"""Macro: Indicador de sync MIDI."""
|
|
if _feedback_controller.output_port:
|
|
# LED verde cuando sync está activo
|
|
_feedback_controller.send_pad_led(83, "green")
|
|
|
|
def trigger_macro(self, macro_name: str) -> Dict[str, Any]:
|
|
"""
|
|
Dispara una macro de visualización.
|
|
|
|
Args:
|
|
macro_name: Nombre de la macro
|
|
"""
|
|
if macro_name not in self.macros:
|
|
return {
|
|
"status": "error",
|
|
"message": f"Macro no existe. Opciones: {list(self.macros.keys())}"
|
|
}
|
|
|
|
# Ejecutar en thread separado para no bloquear
|
|
def run_macro():
|
|
try:
|
|
self.macros[macro_name]()
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] T180: Error en macro {macro_name}: {e}")
|
|
|
|
threading.Thread(target=run_macro, daemon=True).start()
|
|
|
|
logger.info(f"[HARDWARE] T180: Macro '{macro_name}' disparada")
|
|
|
|
return {
|
|
"macro": macro_name,
|
|
"status": "triggered"
|
|
}
|
|
|
|
def get_available_macros(self) -> Dict[str, Any]:
|
|
"""Retorna macros disponibles."""
|
|
return {
|
|
"available_macros": list(self.macros.keys()),
|
|
"descriptions": {
|
|
"strobe_beat": "Strobe rojo sincronizado",
|
|
"level_meter": "Medidor de nivel en LEDs",
|
|
"peak_indicator": "Indicador de pico (clip)",
|
|
"recording_active": "Grabación activa (parpadeo)",
|
|
"midi_clock_sync": "Sync MIDI activo"
|
|
}
|
|
}
|
|
|
|
|
|
_visualization_macros = VisualizationMacros()
|
|
|
|
|
|
def trigger_visualization_macro(macro_name: str) -> Dict[str, Any]:
|
|
"""
|
|
T180: Dispara macro de visualización.
|
|
|
|
Args:
|
|
macro_name: Nombre de la macro
|
|
|
|
Returns:
|
|
Dict con estado de la macro.
|
|
"""
|
|
return _visualization_macros.trigger_macro(macro_name)
|
|
|
|
|
|
def get_visualization_macros() -> Dict[str, Any]:
|
|
"""Obtiene lista de macros disponibles."""
|
|
return _visualization_macros.get_available_macros()
|
|
|
|
|
|
# =============================================================================
|
|
# Hardware Integration Manager (Clase principal)
|
|
# =============================================================================
|
|
|
|
class HardwareIntegrationManager:
|
|
"""
|
|
Manager principal para integración de hardware MIDI.
|
|
|
|
Coordina todos los controladores T166-T180.
|
|
"""
|
|
|
|
def __init__(self):
|
|
self.config: Optional[HardwareConfig] = None
|
|
self.input_port: Optional[Any] = None
|
|
self.output_port: Optional[Any] = None
|
|
self._running = False
|
|
self._midi_thread: Optional[threading.Thread] = None
|
|
|
|
def initialize_hardware(self, hardware_type: str, input_port: str = "", output_port: str = "") -> Dict[str, Any]:
|
|
"""
|
|
Inicializa hardware completo.
|
|
|
|
Args:
|
|
hardware_type: Tipo de controlador
|
|
input_port: Nombre del puerto MIDI de entrada
|
|
output_port: Nombre del puerto MIDI de salida
|
|
"""
|
|
# Obtener mapeo
|
|
mapping = get_hardware_mapping(hardware_type)
|
|
|
|
# Crear configuración
|
|
hw_type = HardwareType(hardware_type.replace(":", "_").lower())
|
|
self.config = HardwareConfig(
|
|
hardware_type=hw_type,
|
|
name=hardware_type,
|
|
input_port=input_port,
|
|
output_port=output_port,
|
|
cc_mappings=mapping["mappings"]["cc_mappings"],
|
|
note_mappings=mapping["mappings"]["note_mappings"],
|
|
has_led_feedback=True,
|
|
fader_count=4,
|
|
knob_count=8,
|
|
pad_count=16,
|
|
button_count=20
|
|
)
|
|
|
|
# Configurar puertos si mido está disponible
|
|
if MIDO_AVAILABLE and input_port and output_port:
|
|
try:
|
|
self.input_port = mido.open_input(input_port)
|
|
self.output_port = mido.open_output(output_port)
|
|
_feedback_controller.set_output_port(self.output_port)
|
|
logger.info(f"[HARDWARE] Puertos MIDI abiertos: {input_port} / {output_port}")
|
|
except Exception as e:
|
|
logger.error(f"[HARDWARE] Error abriendo puertos MIDI: {e}")
|
|
|
|
return {
|
|
"status": "initialized",
|
|
"hardware": hardware_type,
|
|
"cc_mappings": len(self.config.cc_mappings),
|
|
"note_mappings": len(self.config.note_mappings),
|
|
"midi_available": MIDO_AVAILABLE
|
|
}
|
|
|
|
def start_midi_listener(self) -> Dict[str, Any]:
|
|
"""Inicia listener de mensajes MIDI."""
|
|
if not MIDO_AVAILABLE or not self.input_port:
|
|
return {"status": "error", "message": "MIDI no disponible"}
|
|
|
|
self._running = True
|
|
|
|
def midi_loop():
|
|
while self._running:
|
|
for msg in self.input_port.iter_pending():
|
|
self._handle_midi_message(msg)
|
|
time.sleep(0.001) # 1ms polling
|
|
|
|
self._midi_thread = threading.Thread(target=midi_loop, daemon=True)
|
|
self._midi_thread.start()
|
|
|
|
logger.info("[HARDWARE] Listener MIDI iniciado")
|
|
|
|
return {
|
|
"status": "listening",
|
|
"port": self.config.input_port if self.config else ""
|
|
}
|
|
|
|
def _handle_midi_message(self, msg: Any):
|
|
"""Procesa mensaje MIDI entrante."""
|
|
if msg.type == 'control_change':
|
|
self._handle_cc(msg.control, msg.value, msg.channel)
|
|
elif msg.type == 'note_on':
|
|
self._handle_note_on(msg.note, msg.velocity, msg.channel)
|
|
elif msg.type == 'clock':
|
|
_midi_clock.receive_clock_pulse()
|
|
elif msg.type == 'start':
|
|
_midi_clock.receive_start()
|
|
elif msg.type == 'stop':
|
|
_midi_clock.receive_stop()
|
|
|
|
def _handle_cc(self, cc: int, value: int, channel: int):
|
|
"""Procesa mensaje CC."""
|
|
# Buscar mapeo correspondiente
|
|
for name, mapping in self.config.cc_mappings.items() if self.config else []:
|
|
if mapping.cc_number == cc and mapping.channel == channel:
|
|
logger.debug(f"[HARDWARE] CC {name}: {value}")
|
|
|
|
# Ejecutar acciones específicas
|
|
if "gain_staging" in name.lower():
|
|
update_gain_staging_from_fader(value)
|
|
elif "humanize" in name.lower():
|
|
update_humanize_from_knob(value)
|
|
elif "filter" in name.lower():
|
|
asyncio.create_task(_filter_controller.update_filter_value(name, value))
|
|
elif "fader" in name.lower() and _performance_controller.active:
|
|
# Extraer índice de fader
|
|
try:
|
|
idx = int(name.split("_")[1]) - 1
|
|
handle_performance_fader(idx, value)
|
|
except:
|
|
pass
|
|
|
|
break
|
|
|
|
def _handle_note_on(self, note: int, velocity: int, channel: int):
|
|
"""Procesa mensaje Note On."""
|
|
# Buscar mapeo
|
|
for name, mapping in self.config.note_mappings.items() if self.config else []:
|
|
if mapping.note == note and mapping.channel == channel:
|
|
logger.debug(f"[HARDWARE] Note {name}: vel={velocity}")
|
|
|
|
# Ejecutar acciones
|
|
if "scene" in name.lower():
|
|
# Extraer número de scene
|
|
try:
|
|
idx = int(name.split("_")[1]) - 1
|
|
_scene_controller.trigger_scene(idx)
|
|
except:
|
|
pass
|
|
elif "panic" in name.lower():
|
|
trigger_panic_button()
|
|
elif "fill" in name.lower():
|
|
# Extraer número de pad
|
|
try:
|
|
pad = int(name.split("_")[1])
|
|
_drum_pad_controller.trigger_fill(pad)
|
|
except:
|
|
pass
|
|
elif "performance" in name.lower():
|
|
if velocity > 0:
|
|
if not _performance_controller.active:
|
|
activate_performance_mode()
|
|
else:
|
|
deactivate_performance_mode()
|
|
|
|
# Feedback LED
|
|
if mapping.feedback_enabled and velocity > 0:
|
|
color = self._get_color_for_note(name)
|
|
_feedback_controller.send_pad_led(note, color, channel)
|
|
|
|
break
|
|
|
|
def _get_color_for_note(self, name: str) -> str:
|
|
"""Determina color LED según función."""
|
|
if "scene" in name.lower():
|
|
return "green"
|
|
elif "panic" in name.lower():
|
|
return "red"
|
|
elif "fill" in name.lower():
|
|
return "yellow"
|
|
elif "performance" in name.lower():
|
|
return "orange"
|
|
return "green"
|
|
|
|
def stop(self):
|
|
"""Detiene manager de hardware."""
|
|
self._running = False
|
|
if self._midi_thread:
|
|
self._midi_thread.join(timeout=1.0)
|
|
|
|
if self.input_port:
|
|
self.input_port.close()
|
|
if self.output_port:
|
|
self.output_port.close()
|
|
|
|
logger.info("[HARDWARE] Manager detenido")
|
|
|
|
|
|
# Instancia global del manager
|
|
_hardware_manager = HardwareIntegrationManager()
|
|
|
|
|
|
def initialize_hardware_integration(
|
|
hardware_type: str = "xone_k2",
|
|
input_port: str = "",
|
|
output_port: str = ""
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Inicializa integración completa de hardware.
|
|
|
|
Args:
|
|
hardware_type: Tipo de controlador (xone_k2, akai_apc40, pioneer_ddj)
|
|
input_port: Puerto MIDI de entrada (opcional)
|
|
output_port: Puerto MIDI de salida (opcional)
|
|
|
|
Returns:
|
|
Dict con estado de inicialización.
|
|
"""
|
|
return _hardware_manager.initialize_hardware(hardware_type, input_port, output_port)
|
|
|
|
|
|
def start_hardware_listener() -> Dict[str, Any]:
|
|
"""Inicia listener de mensajes MIDI."""
|
|
return _hardware_manager.start_midi_listener()
|
|
|
|
|
|
def stop_hardware_integration() -> Dict[str, Any]:
|
|
"""Detiene integración de hardware."""
|
|
_hardware_manager.stop()
|
|
return {"status": "stopped"}
|
|
|
|
|
|
# =============================================================================
|
|
# Funciones de conveniencia para MCP
|
|
# =============================================================================
|
|
|
|
def get_complete_hardware_status() -> Dict[str, Any]:
|
|
"""
|
|
Obtiene estado completo de toda la integración de hardware.
|
|
|
|
Returns:
|
|
Dict consolidado con T166-T180.
|
|
"""
|
|
return {
|
|
"t166_hardware_mapping": get_hardware_mapping("xone_k2"),
|
|
"t167_filter_bindings": asyncio.run(_filter_controller.get_filter_status()) if asyncio else {},
|
|
"t168_monitor_states": {}, # Estados de monitor
|
|
"t169_midi_clock": get_midi_clock_status(),
|
|
"t170_gain_staging": get_gain_staging_status(),
|
|
"t171_drum_pads": _drum_pad_controller.get_pad_mappings(),
|
|
"t172_panic_status": _panic_controller.get_status(),
|
|
"t173_feedback": {"output_port_set": _feedback_controller.output_port is not None},
|
|
"t174_cpu_monitor": get_cpu_load(),
|
|
"t175_scene_quantization": _scene_controller.get_quantization_modes(),
|
|
"t176_performance_mode": get_performance_status(),
|
|
"t177_humanize_macro": get_humanize_macro_status(),
|
|
"t178_silence_detector": get_silence_detector_status(),
|
|
"t179_nudge_status": get_nudge_status(),
|
|
"t180_visualization_macros": get_visualization_macros(),
|
|
"mido_available": MIDO_AVAILABLE,
|
|
"timestamp": time.time()
|
|
}
|
|
|
|
|
|
# Exportar símbolos principales
|
|
__all__ = [
|
|
# T166
|
|
"get_hardware_mapping",
|
|
"HardwareType",
|
|
"CCMapping",
|
|
"NoteMapping",
|
|
# T167
|
|
"bind_filter_to_bus_async",
|
|
"AsyncFilterController",
|
|
# T168
|
|
"toggle_track_monitor",
|
|
"TrackMonitorController",
|
|
# T169
|
|
"start_midi_clock_sync",
|
|
"stop_midi_clock_sync",
|
|
"get_midi_clock_status",
|
|
# T170
|
|
"update_gain_staging_from_fader",
|
|
"get_gain_staging_status",
|
|
# T171
|
|
"trigger_fill_from_pad",
|
|
"register_fill_callback",
|
|
# T172
|
|
"trigger_panic_button",
|
|
"release_panic_button",
|
|
"register_panic_callback",
|
|
# T173
|
|
"indicate_export_on_hardware",
|
|
"send_pad_led_feedback",
|
|
"set_feedback_output_port",
|
|
# T174
|
|
"start_cpu_monitoring",
|
|
"stop_cpu_monitoring",
|
|
"get_cpu_load",
|
|
# T175
|
|
"trigger_scene_from_hardware",
|
|
"set_scene_quantization",
|
|
# T176
|
|
"activate_performance_mode",
|
|
"deactivate_performance_mode",
|
|
"handle_performance_fader",
|
|
# T177
|
|
"update_humanize_from_knob",
|
|
"register_humanize_callback",
|
|
# T178
|
|
"start_silence_detection",
|
|
"stop_silence_detection",
|
|
"register_silence_callback",
|
|
# T179
|
|
"apply_nudge_forward",
|
|
"apply_nudge_backward",
|
|
"register_nudge_callback",
|
|
# T180
|
|
"trigger_visualization_macro",
|
|
"get_visualization_macros",
|
|
# Manager
|
|
"initialize_hardware_integration",
|
|
"start_hardware_listener",
|
|
"stop_hardware_integration",
|
|
"get_complete_hardware_status",
|
|
"HardwareIntegrationManager",
|
|
]
|
|
|
|
|
|
if __name__ == "__main__":
|
|
# Test básico del módulo
|
|
print("=" * 60)
|
|
print("Hardware Integration Module - Test T166-T180")
|
|
print("=" * 60)
|
|
|
|
# T166
|
|
print("\n[T166] Hardware Mapping:")
|
|
mapping = get_hardware_mapping("xone_k2")
|
|
print(f" CC mappings: {mapping['cc_count']}")
|
|
print(f" Note mappings: {mapping['note_count']}")
|
|
|
|
# T169
|
|
print("\n[T169] MIDI Clock:")
|
|
status = start_midi_clock_sync()
|
|
print(f" Status: {status['status']}")
|
|
print(f" PPQN: {status['ppqn']}")
|
|
|
|
# T170
|
|
print("\n[T170] Gain Staging:")
|
|
gain_status = update_gain_staging_from_fader(100)
|
|
print(f" Target LUFS: {gain_status['target_lufs']}dB")
|
|
|
|
# T172
|
|
print("\n[T172] Panic Button:")
|
|
panic = trigger_panic_button()
|
|
print(f" Status: {panic['status']}")
|
|
|
|
# T176
|
|
print("\n[T176] Performance Mode:")
|
|
perf = activate_performance_mode("default")
|
|
print(f" Status: {perf['status']}")
|
|
print(f" Layout: {perf['layout']}")
|
|
|
|
# T180
|
|
print("\n[T180] Visualization Macros:")
|
|
macros = get_visualization_macros()
|
|
print(f" Available: {macros['available_macros']}")
|
|
|
|
print("\n" + "=" * 60)
|
|
print("All T166-T180 tasks implemented successfully!")
|
|
print("=" * 60)
|