Files
ableton-mcp-ai/AbletonMCP_AI/AbletonMCP_AI/MCP_Server/hardware_integration.py

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)