Files
ableton-mcp-ai/AbletonMCP_AI_BAK_20260328_200801/Remote_Script.py
renato97 6ec8663954 Initial commit: AbletonMCP-AI complete system
- MCP Server with audio fallback, sample management
- Song generator with bus routing
- Reference listener and audio resampler
- Vector-based sample search
- Master chain with limiter and calibration
- Fix: Audio fallback now works without M4L
- Fix: Full song detection in sample loader

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-28 22:53:10 -03:00

944 lines
34 KiB
Python

"""
AbletonMCP AI - Remote Script para Ableton Live 12
Integración completa con MCP para generación musical por IA
Este script debe copiarse a:
C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\
Y luego seleccionarse en Preferencias > Link/Tempo/MIDI > Control Surface
"""
from __future__ import absolute_import, print_function, unicode_literals
from _Framework.ControlSurface import ControlSurface
import socket
import json
import threading
import time
import traceback
import os
import hashlib
# Python 2/3 compatibility
try:
import queue
except ImportError:
pass
try:
string_types = basestring
except NameError:
string_types = str
# Configuración
DEFAULT_PORT = 9877
HOST = "localhost"
CONFIG_FILE = r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\track_config.json"
def create_instance(c_instance):
"""Crea y retorna la instancia del script"""
return AbletonMCP_AI(c_instance)
class AbletonMCP_AI(ControlSurface):
"""
Remote Script para integración MCP + AI con Ableton Live 12
Características:
- Servidor socket para comunicación con MCP Server
- Generación de tracks MIDI con patrones automáticos
- Carga de samples vía browser
- Integración con análisis de audio por IA
"""
def __init__(self, c_instance):
ControlSurface.__init__(self, c_instance)
self.log_message("=" * 60)
self.log_message("AbletonMCP AI - Inicializando...")
self.log_message("=" * 60)
# Referencia a la canción
self._song = self.song()
# Servidor socket
self.server = None
self.client_threads = []
self.server_thread = None
self.running = False
# Config watcher para generación automática
self._last_config_hash = None
self._config_watcher_thread = None
self._config_watcher_running = False
# Iniciar servidor
self.start_server()
# Iniciar watcher de configuración
self.start_config_watcher()
self.log_message("AbletonMCP AI inicializado correctamente")
self.show_message("AbletonMCP AI: Listo en puerto " + str(DEFAULT_PORT))
def disconnect(self):
"""Llamado cuando Ableton cierra o se remueve el script"""
self.log_message("AbletonMCP AI desconectando...")
self.running = False
self._config_watcher_running = False
# Detener servidor
if self.server:
try:
self.server.close()
except Exception:
pass
# Esperar threads
if self.server_thread and self.server_thread.is_alive():
self.server_thread.join(1.0)
if self._config_watcher_thread and self._config_watcher_thread.is_alive():
self._config_watcher_thread.join(0.5)
ControlSurface.disconnect(self)
self.log_message("AbletonMCP AI desconectado")
# =========================================================================
# SERVIDOR SOCKET
# =========================================================================
def start_server(self):
"""Inicia el servidor socket en un thread separado"""
try:
self.server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
self.server.bind((HOST, DEFAULT_PORT))
self.server.listen(5)
self.running = True
self.server_thread = threading.Thread(target=self._server_thread)
self.server_thread.daemon = True
self.server_thread.start()
self.log_message("Servidor socket iniciado en puerto " + str(DEFAULT_PORT))
except Exception as e:
self.log_message("Error iniciando servidor: " + str(e))
self.show_message("AbletonMCP AI Error: " + str(e))
def _server_thread(self):
"""Thread principal del servidor - maneja conexiones"""
try:
self.server.settimeout(1.0)
while self.running:
try:
client, address = self.server.accept()
self.log_message("Conexión aceptada de " + str(address))
# Manejar cliente en thread separado
client_thread = threading.Thread(
target=self._handle_client,
args=(client,)
)
client_thread.daemon = True
client_thread.start()
self.client_threads.append(client_thread)
# Limpiar threads terminados
self.client_threads = [t for t in self.client_threads if t.is_alive()]
except socket.timeout:
continue
except Exception as e:
if self.running:
self.log_message("Error servidor: " + str(e))
time.sleep(0.5)
except Exception as e:
self.log_message("Error thread servidor: " + str(e))
def _handle_client(self, client):
"""Maneja comunicación con un cliente conectado"""
client.settimeout(None)
buffer = ''
try:
while self.running:
try:
data = client.recv(8192)
if not data:
self.log_message("Cliente desconectado")
break
# Acumular en buffer
try:
buffer += data.decode('utf-8')
except AttributeError:
buffer += data
# Intentar parsear JSON
try:
command = json.loads(buffer)
buffer = ''
self.log_message("Comando recibido: " + str(command.get("type", "unknown")))
# Procesar comando
response = self._process_command(command)
# Enviar respuesta
try:
client.sendall(json.dumps(response).encode('utf-8'))
except AttributeError:
client.sendall(json.dumps(response))
except ValueError:
# Datos incompletos, esperar más
continue
except Exception as e:
self.log_message("Error manejando cliente: " + str(e))
error_response = {"status": "error", "message": str(e)}
try:
client.sendall(json.dumps(error_response).encode('utf-8'))
except Exception:
pass
break
finally:
try:
client.close()
except Exception:
pass
# =========================================================================
# CONFIG WATCHER - Generación automática
# =========================================================================
def start_config_watcher(self):
"""Inicia el watcher de configuración para generación automática"""
self._config_watcher_running = True
self._config_watcher_thread = threading.Thread(target=self._config_watcher_loop)
self._config_watcher_thread.daemon = True
self._config_watcher_thread.start()
self.log_message("Config watcher iniciado")
def _config_watcher_loop(self):
"""Loop que monitorea cambios en el archivo de configuración"""
while self._config_watcher_running:
try:
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, 'r') as f:
content = f.read()
h = hashlib.md5(content.encode()).hexdigest()
if h != self._last_config_hash:
self._last_config_hash = h
self.log_message("Config cambiado - generando track...")
try:
config = json.loads(content)
# Solo procesar si tiene flag 'auto_generate'
if config.get('auto_generate', False):
self._generate_from_config(config)
except Exception as e:
self.log_message("Error generando desde config: " + str(e))
self.log_message(traceback.format_exc())
time.sleep(1.0) # Revisar cada segundo
except Exception as e:
self.log_message("Error en config watcher: " + str(e))
time.sleep(2.0)
def _generate_from_config(self, config):
"""Genera un track completo desde una configuración"""
try:
self.show_message("AI: Generando " + config.get('name', 'Track'))
# 1. Limpiar proyecto existente
self._clear_all_tracks()
# 2. Setear BPM
bpm = config.get('bpm', 128)
self._song.tempo = bpm
# 3. Crear tracks según configuración
tracks_config = config.get('tracks', [])
for idx, track_cfg in enumerate(tracks_config):
track_type = track_cfg.get('type', 'midi')
name = track_cfg.get('name', 'Track ' + str(idx))
if track_type == 'midi':
self._song.create_midi_track(idx)
elif track_type == 'audio':
self._song.create_audio_track(idx)
track = self._song.tracks[idx]
track.name = name
# Setear color si existe
if 'color' in track_cfg:
track.color = track_cfg['color']
# Crear clip con notas si existe configuración
if 'clip' in track_cfg:
clip_cfg = track_cfg['clip']
slot_idx = clip_cfg.get('slot', 0)
length = clip_cfg.get('length', 4.0)
# Asegurar que existan suficientes scenes
while len(self._song.scenes) <= slot_idx:
self._song.create_scene(-1)
clip_slot = track.clip_slots[slot_idx]
clip_slot.create_clip(length)
# Agregar notas
if 'notes' in clip_cfg:
clip = clip_slot.clip
for note in clip_cfg['notes']:
pitch = note.get('pitch', 60)
start = note.get('start', 0.0)
duration = note.get('duration', 0.25)
velocity = note.get('velocity', 100)
clip.add_new_note((pitch, start, duration, velocity, False))
# Cargar instrumento si se especifica
if 'instrument' in track_cfg:
instrument_name = track_cfg['instrument']
# Usar browser para cargar
self._load_instrument_by_name(track, instrument_name)
self.show_message("AI: Track generado exitosamente!")
self.log_message("Generación completada: " + str(len(tracks_config)) + " tracks")
except Exception as e:
self.log_message("Error en generación: " + str(e))
self.log_message(traceback.format_exc())
self.show_message("AI Error: " + str(e))
def _clear_all_tracks(self):
"""Elimina todos los tracks existentes"""
try:
while len(self._song.tracks) > 0:
self._song.delete_track(len(self._song.tracks) - 1)
except Exception as e:
self.log_message("Error limpiando tracks: " + str(e))
def _load_instrument_by_name(self, track, name):
"""Carga un instrumento en el track por nombre"""
try:
browser = self.application().browser
# Buscar en categorías de instrumentos
if hasattr(browser, 'instruments'):
for item in self._search_browser_items(browser.instruments, name):
try:
browser.load_item(item)
self.log_message("Instrumento cargado: " + name)
return True
except Exception as e:
self.log_message("Error cargando instrumento: " + str(e))
return False
except Exception as e:
self.log_message("Error buscando instrumento: " + str(e))
return False
def _search_browser_items(self, root, name, depth=0, max_depth=5):
"""Busca items en el browser recursivamente"""
if depth > max_depth or root is None:
return []
results = []
try:
# Verificar si el nombre coincide
item_name = getattr(root, 'name', '').lower()
if name.lower() in item_name or item_name in name.lower():
results.append(root)
# Buscar en hijos
if hasattr(root, 'children'):
for child in root.children:
results.extend(self._search_browser_items(child, name, depth + 1, max_depth))
except Exception:
pass
return results
# =========================================================================
# PROCESAMIENTO DE COMANDOS
# =========================================================================
def _process_command(self, command):
"""Procesa un comando recibido y retorna respuesta"""
command_type = command.get("type", "")
params = command.get("params", {})
try:
# Comandos de información
if command_type == "get_session_info":
return self._cmd_get_session_info()
elif command_type == "get_track_info":
return self._cmd_get_track_info(params)
elif command_type == "get_tracks":
return self._cmd_get_tracks()
# Comandos de tracks
elif command_type == "create_midi_track":
return self._cmd_create_midi_track(params)
elif command_type == "create_audio_track":
return self._cmd_create_audio_track(params)
elif command_type == "set_track_name":
return self._cmd_set_track_name(params)
elif command_type == "set_track_volume":
return self._cmd_set_track_volume(params)
elif command_type == "set_track_pan":
return self._cmd_set_track_pan(params)
elif command_type == "set_track_mute":
return self._cmd_set_track_mute(params)
elif command_type == "set_track_solo":
return self._cmd_set_track_solo(params)
elif command_type == "set_track_color":
return self._cmd_set_track_color(params)
# Comandos de clips
elif command_type == "create_clip":
return self._cmd_create_clip(params)
elif command_type == "add_notes_to_clip":
return self._cmd_add_notes_to_clip(params)
elif command_type == "set_clip_name":
return self._cmd_set_clip_name(params)
elif command_type == "set_clip_envelope":
return self._cmd_set_clip_envelope(params)
elif command_type == "fire_clip":
return self._cmd_fire_clip(params)
elif command_type == "stop_clip":
return self._cmd_stop_clip(params)
# Comandos de transporte
elif command_type == "set_tempo":
return self._cmd_set_tempo(params)
elif command_type == "start_playback":
return self._cmd_start_playback()
elif command_type == "stop_playback":
return self._cmd_stop_playback()
# Comandos de escenas
elif command_type == "create_scene":
return self._cmd_create_scene(params)
elif command_type == "set_scene_name":
return self._cmd_set_scene_name(params)
elif command_type == "fire_scene":
return self._cmd_fire_scene(params)
# Comandos de dispositivos
elif command_type == "load_instrument_or_effect":
return self._cmd_load_instrument(params)
elif command_type == "set_device_parameter":
return self._cmd_set_device_parameter(params)
# Comando de generación AI
elif command_type == "generate_track":
return self._cmd_generate_track(params)
else:
return {"status": "error", "message": "Comando desconocido: " + command_type}
except Exception as e:
self.log_message("Error procesando comando " + command_type + ": " + str(e))
self.log_message(traceback.format_exc())
return {"status": "error", "message": str(e)}
# =========================================================================
# IMPLEMENTACIÓN DE COMANDOS
# =========================================================================
def _cmd_get_session_info(self):
"""Retorna información de la sesión actual"""
return {
"status": "success",
"result": {
"tempo": self._song.tempo,
"signature_numerator": self._song.signature_numerator,
"signature_denominator": self._song.signature_denominator,
"is_playing": self._song.is_playing,
"current_song_time": self._song.current_song_time,
"loop_start": self._song.loop_start,
"loop_length": self._song.loop_length,
"num_tracks": len(self._song.tracks),
"num_scenes": len(self._song.scenes),
"num_return_tracks": len(self._song.return_tracks)
}
}
def _cmd_get_track_info(self, params):
"""Retorna información de un track específico"""
idx = params.get("track_index", 0)
if idx < 0 or idx >= len(self._song.tracks):
return {"status": "error", "message": "Track index fuera de rango"}
track = self._song.tracks[idx]
# Determinar tipo de track
track_type = "unknown"
if track.has_midi_input:
track_type = "midi"
elif track.has_audio_input:
track_type = "audio"
return {
"status": "success",
"result": {
"index": idx,
"name": track.name,
"type": track_type,
"color": track.color,
"mute": track.mute,
"solo": track.solo,
"arm": track.arm,
"volume": track.mixer_device.volume.value if track.mixer_device else 0.85,
"pan": track.mixer_device.panning.value if track.mixer_device else 0.0,
"num_clips": len(track.clip_slots),
"num_devices": len(track.devices)
}
}
def _cmd_get_tracks(self):
"""Retorna lista de todos los tracks"""
tracks = []
for i, track in enumerate(self._song.tracks):
track_type = "midi" if track.has_midi_input else "audio" if track.has_audio_input else "unknown"
tracks.append({
"index": i,
"name": track.name,
"type": track_type,
"color": track.color,
"mute": track.mute,
"solo": track.solo
})
return {"status": "success", "result": tracks}
def _cmd_create_midi_track(self, params):
"""Crea un track MIDI"""
index = params.get("index", -1)
self._song.create_midi_track(index)
return {"status": "success", "result": {"message": "MIDI track creado", "index": index}}
def _cmd_create_audio_track(self, params):
"""Crea un track de audio"""
index = params.get("index", -1)
self._song.create_audio_track(index)
return {"status": "success", "result": {"message": "Audio track creado", "index": index}}
def _cmd_set_track_name(self, params):
"""Setea el nombre de un track"""
idx = params.get("track_index", 0)
name = params.get("name", "Track")
self._song.tracks[idx].name = name
return {"status": "success", "result": {"message": "Nombre actualizado", "name": name}}
def _cmd_set_track_volume(self, params):
"""Setea el volumen de un track"""
idx = params.get("track_index", 0)
volume = params.get("volume", 0.85)
track = self._song.tracks[idx]
if track.mixer_device and track.mixer_device.volume:
track.mixer_device.volume.value = volume
return {"status": "success"}
def _cmd_set_track_pan(self, params):
"""Setea el pan de un track"""
idx = params.get("track_index", 0)
pan = params.get("pan", 0.0)
track = self._song.tracks[idx]
if track.mixer_device and track.mixer_device.panning:
track.mixer_device.panning.value = pan
return {"status": "success"}
def _cmd_set_track_mute(self, params):
"""Setea el mute de un track"""
idx = params.get("track_index", 0)
mute = params.get("mute", True)
track = self._song.tracks[idx]
current_mute = track.mute
if current_mute != mute:
track.mute = mute
return {"status": "success", "result": {"mute": track.mute, "track_index": idx}}
def _cmd_set_track_solo(self, params):
"""Setea el solo de un track"""
idx = params.get("track_index", 0)
solo = params.get("solo", True)
self._song.tracks[idx].solo = solo
return {"status": "success"}
def _cmd_set_track_color(self, params):
"""Setea el color de un track"""
idx = params.get("track_index", 0)
color = params.get("color", 0)
self._song.tracks[idx].color = color
return {"status": "success"}
def _cmd_create_clip(self, params):
"""Crea un clip en un slot"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
length = params.get("length", 4.0)
track = self._song.tracks[track_idx]
# Asegurar que existan suficientes scenes
while len(self._song.scenes) <= clip_idx:
self._song.create_scene(-1)
clip_slot = track.clip_slots[clip_idx]
clip_slot.create_clip(length)
return {"status": "success", "result": {"message": "Clip creado"}}
def _cmd_add_notes_to_clip(self, params):
"""Agrega notas a un clip MIDI"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
notes = params.get("notes", [])
track = self._song.tracks[track_idx]
clip_slot = track.clip_slots[clip_idx]
if not clip_slot.has_clip:
return {"status": "error", "message": "No hay clip en este slot"}
clip = clip_slot.clip
for note in notes:
pitch = note.get("pitch", 60)
start = note.get("start", 0.0)
duration = note.get("duration", 0.25)
velocity = note.get("velocity", 100)
clip.add_new_note((pitch, start, duration, velocity, False))
return {"status": "success", "result": {"num_notes_added": len(notes)}}
def _cmd_set_clip_name(self, params):
"""Setea el nombre de un clip"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
name = params.get("name", "Clip")
clip_slot = self._song.tracks[track_idx].clip_slots[clip_idx]
if clip_slot.has_clip:
clip_slot.clip.name = name
return {"status": "success"}
def _cmd_fire_clip(self, params):
"""Dispara un clip"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
clip_slot = self._song.tracks[track_idx].clip_slots[clip_idx]
clip_slot.fire()
return {"status": "success"}
def _cmd_stop_clip(self, params):
"""Detiene un clip"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
clip_slot = self._song.tracks[track_idx].clip_slots[clip_idx]
clip_slot.stop()
return {"status": "success"}
def _cmd_set_tempo(self, params):
"""Setea el BPM"""
tempo = params.get("tempo", 120.0)
self._song.tempo = tempo
return {"status": "success", "result": {"tempo": tempo}}
def _cmd_start_playback(self):
"""Inicia reproducción"""
self._song.start_playing()
return {"status": "success"}
def _cmd_stop_playback(self):
"""Detiene reproducción"""
self._song.stop_playing()
return {"status": "success"}
def _cmd_create_scene(self, params):
"""Crea una scene"""
index = params.get("index", -1)
self._song.create_scene(index)
return {"status": "success"}
def _cmd_set_scene_name(self, params):
"""Setea el nombre de una scene"""
idx = params.get("scene_index", 0)
name = params.get("name", "Scene")
self._song.scenes[idx].name = name
return {"status": "success"}
def _cmd_fire_scene(self, params):
"""Dispara una scene"""
idx = params.get("scene_index", 0)
scene = self._song.scenes[idx]
scene.fire()
if not self._song.is_playing:
self._song.start_playing()
return {"status": "success"}
def _cmd_load_instrument(self, params):
"""Carga un instrumento en un track"""
track_idx = params.get("track_index", 0)
name = params.get("name", "")
track = self._song.tracks[track_idx]
success = self._load_instrument_by_name(track, name)
if success:
return {"status": "success", "result": {"message": "Instrumento cargado"}}
else:
return {"status": "error", "message": "No se pudo cargar el instrumento"}
def _cmd_set_device_parameter(self, params):
"""Setea un parámetro de dispositivo"""
track_idx = params.get("track_index", 0)
device_idx = params.get("device_index", 0)
param_idx = params.get("parameter_index", 0)
value = params.get("value", 0.0)
track = self._song.tracks[track_idx]
device = track.devices[device_idx]
param = device.parameters[param_idx]
param.value = value
return {"status": "success"}
def _cmd_generate_track(self, params):
"""Comando principal de generación de tracks"""
# Este comando delega a _generate_from_config
# pero puede ser llamado directamente vía socket
try:
self._generate_from_config(params)
return {"status": "success", "result": {"message": "Track generado exitosamente"}}
except Exception as e:
return {"status": "error", "message": str(e)}
def _cmd_set_clip_envelope(self, params):
"""Setea un envelope (volume, pan, send) en un clip con puntos de automatización"""
track_idx = params.get("track_index", 0)
clip_idx = params.get("clip_index", 0)
envelope_name = params.get("envelope", "volume") # volume, pan, send
points = params.get("points", [])
track = self._song.tracks[track_idx]
clip_slot = track.clip_slots[clip_idx]
if not clip_slot.has_clip:
return {"status": "error", "message": "No hay clip en este slot"}
clip = clip_slot.clip
# Obtener el envelope correcto
if envelope_name == "volume":
envelope = clip.volume_envelope
elif envelope_name == "pan":
envelope = clip.pan_envelope
elif envelope_name == "send":
send_idx = params.get("send_index", 0)
if send_idx < len(track.mixer_device.sends):
envelope = track.mixer_device.sends[send_idx].envelope
else:
return {"status": "error", "message": "Send index fuera de rango"}
else:
return {"status": "error", "message": "Envelope type desconocido: " + envelope_name}
# Limpiar puntos existentes si se especifica
clear_existing = params.get("clear_existing", False)
if clear_existing:
while len(envelope.points) > 0:
envelope.delete_point(len(envelope.points) - 1)
# Agregar puntos de automatización desde el array de puntos
if points:
for point in points:
if isinstance(point, dict):
time_pos = point.get("time", 0.0)
value = point.get("value", 0.0)
envelope.add_new_point(time_pos, value)
return {"status": "success", "result": {"message": "Envelope seteado con puntos", "points_added": len(points)}}
else:
return {"status": "error", "message": "No se especificaron puntos de automatización"}
def _cmd_calibrate_track_gain(self, params):
"""Calibra el gain de un track basado en loudness"""
track_idx = params.get("track_index", 0)
target_loudness = params.get("target_loudness", -14.0) # LUFS target
measurement_window = params.get("measurement_window", 0.1) # segundos
track = self._song.tracks[track_idx]
if not track.has_audio_input:
return {"status": "error", "message": "Track no es de audio"}
# Obtener el peak volume actual
current_volume = track.mixer_device.volume.value
# Calibrar para alcanzar el target (simplificado)
# En una implementación real, usaríamos análisis de loudness real
# Por ahora, ajustamos proporcionalmente
adjustment = target_loudness / -20.0 # Aproximación
new_volume = max(0.0, min(1.0, current_volume * adjustment))
track.mixer_device.volume.value = new_volume
return {
"status": "success",
"result": {
"message": "Gain calibrado",
"current_volume": current_volume,
"new_volume": new_volume,
"target_loudness": target_loudness
}
}
def _cmd_apply_compression(self, params):
"""Aplica compresión a un track"""
track_idx = params.get("track_index", 0)
threshold = params.get("threshold", -24.0)
ratio = params.get("ratio", 4.0)
attack = params.get("attack", 0.01)
release = params.get("release", 0.1)
track = self._song.tracks[track_idx]
# Buscar o crear compressor
compressor = None
for device in track.devices:
if device.name == "Compressor":
compressor = device
break
if compressor is None:
# Intentar cargar Compressor desde browser
browser = self.application().browser
for item in self._search_browser_items(browser.effects, "Compressor"):
try:
browser.load_item(item)
compressor = track.devices[-1]
break
except Exception:
pass
if compressor:
# Setear parámetros (índices pueden variar según versión)
try:
if len(compressor.parameters) > 0:
compressor.parameters[0].value = threshold # Threshold
if len(compressor.parameters) > 1:
compressor.parameters[1].value = ratio # Ratio
if len(compressor.parameters) > 2:
compressor.parameters[2].value = attack # Attack
if len(compressor.parameters) > 3:
compressor.parameters[3].value = release # Release
except Exception:
pass
return {"status": "success", "result": {"message": "Compresor aplicado"}}
else:
return {"status": "error", "message": "No se pudo cargar compresor"}
def _cmd_apply_limiting(self, params):
"""Aplica limiting para loudness normalization"""
track_idx = params.get("track_index", 0)
target_loudness = params.get("target_loudness", -1.0) # LUFS para master
lookahead = params.get("lookahead", 0.01)
release = params.get("release", 0.05)
track = self._song.tracks[track_idx]
# Buscar o crear limiter
limiter = None
for device in track.devices:
if "Limiter" in device.name:
limiter = device
break
if limiter is None:
# Intentar cargar Limiter desde browser
browser = self.application().browser
for item in self._search_browser_items(browser.effects, "Limiter"):
try:
browser.load_item(item)
limiter = track.devices[-1]
break
except Exception:
pass
if limiter:
# Setear parámetros
try:
if len(limiter.parameters) > 0:
limiter.parameters[0].value = target_loudness # Gain
if len(limiter.parameters) > 1:
limiter.parameters[1].value = lookahead # Lookahead
if len(limiter.parameters) > 2:
limiter.parameters[2].value = release # Release
except Exception:
pass
return {"status": "success", "result": {"message": "Limiter aplicado"}}
else:
return {"status": "error", "message": "No se pudo cargar limiter"}
def _cmd_master_loudness_normalization(self, params):
"""Normaliza el loudness del master track"""
track_idx = params.get("track_index", 0)
target_loudness = params.get("target_loudness", -14.0)
track = self._song.tracks[track_idx]
# Calibrar gain
current_volume = track.mixer_device.volume.value
adjustment = 10 ** ((target_loudness - (-14)) / 20) # Aproximación
new_volume = max(0.0, min(1.0, current_volume * adjustment))
track.mixer_device.volume.value = new_volume
return {
"status": "success",
"result": {
"message": "Loudness normalizado",
"target_loudness": target_loudness,
"new_volume": new_volume
}
}