- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain - Method 1: track.insert_arrangement_clip() [Live 12+] - Method 2: track.create_audio_clip() [Live 11+] - Method 3: arrangement_clips.add_new_clip() [Live 12+] - Method 4: Session->duplicate_clip_to_arrangement [Legacy] - Method 5: Session->Recording [Universal] - Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow - Update skills documentation - Verified: 3 clips created at positions [0, 4, 8] in Arrangement View Closes: Audio injection in Arrangement View
494 lines
16 KiB
Markdown
494 lines
16 KiB
Markdown
# ANÁLISIS CRÍTICO - AbletonMCP_AI v2.0
|
|
|
|
> **Fecha**: 2026-04-11
|
|
> **Agentes desplegados**: 5 (análisis paralelo)
|
|
> **Archivo analizado**: `AbletonMCP_AI/__init__.py` (4,428 líneas)
|
|
> **Problema**: Clips no visibles en Arrangement View
|
|
> **Estado**: CRÍTICO - Requiere fixes inmediatos
|
|
|
|
---
|
|
|
|
## RESUMEN EJECUTIVO
|
|
|
|
**Diagnóstico**: El sistema MCP está **funcional técnicamente** pero tiene **problemas de integración con la UI de Ableton Live 12**.
|
|
|
|
| Problema | Causa Raíz | Impacto |
|
|
|----------|-----------|---------|
|
|
| **Clips no visibles** | Se crean en Session View, usuario ve Arrangement View | 🔴 CRÍTICO |
|
|
| **`produce_with_library: 0`** | `SampleSelector` no encuentra samples | 🟡 ALTO |
|
|
| **Arrangement handlers engañosos** | Nombre dice "arrangement" pero crea en Session | 🟡 ALTO |
|
|
| **Race condition en dispatch** | Tareas se encolan pero UI puede no refrescar | 🟠 MEDIO |
|
|
| **Inconsistencias de reporte** | Diferentes tools reportan diferentes cantidades de tracks | 🟠 MEDIO |
|
|
|
|
---
|
|
|
|
## PROBLEMA #1: Clips Creados en Session View (NO Arrangement)
|
|
|
|
### 🔴 CRÍTICO - Usuario no ve contenido
|
|
|
|
**Estado Actual**:
|
|
- ✅ Comandos retornan "success"
|
|
- ✅ Tracks se crean correctamente
|
|
- ❌ **Clips NO visibles en Arrangement View**
|
|
- ❌ **Usuario no puede ver ni escuchar el contenido**
|
|
|
|
### Análisis Técnico
|
|
|
|
**Handler**: `_cmd_generate_midi_clip()` (líneas 1,816-1,860)
|
|
|
|
```python
|
|
def _cmd_generate_midi_clip(self, track_index, clip_index, notes, **kw):
|
|
t = self._song.tracks[int(track_index)]
|
|
slot = t.clip_slots[int(clip_index)] # ← SESSION VIEW
|
|
|
|
if slot.has_clip:
|
|
slot.delete_clip()
|
|
|
|
slot.create_clip(float(clip_length)) # ← CREA EN SESSION
|
|
slot.clip.set_notes(tuple(live_notes)) # ← NOTAS EN SESSION
|
|
```
|
|
|
|
**Handler**: `_cmd_load_sample_direct()` (líneas 3,822-3,877)
|
|
|
|
```python
|
|
def _cmd_load_sample_direct(self, track_index, file_path, slot_index=0, ...):
|
|
t = self._song.tracks[int(track_index)]
|
|
slot = t.clip_slots[int(slot_index)] # ← SESSION VIEW
|
|
|
|
clip = slot.create_audio_clip(fpath) # ← CREA EN SESSION
|
|
```
|
|
|
|
**La API de Ableton Live Python NO tiene método directo para crear clips en Arrangement View.**
|
|
|
|
La única forma es:
|
|
1. Crear clips en Session View (`clip_slots`)
|
|
2. Activar `arrangement_overdub = True`
|
|
3. Disparar clips con `slot.fire()`
|
|
4. Live captura automáticamente a Arrangement durante playback
|
|
|
|
### Solución Propuesta
|
|
|
|
#### Opción A: Parámetro `arrangement=True` (Recomendada)
|
|
|
|
Modificar `_cmd_generate_midi_clip()` para intentar primero Arrangement:
|
|
|
|
```python
|
|
def _cmd_generate_midi_clip(self, track_index, clip_index, notes,
|
|
arrangement=False, start_time=0.0, **kw):
|
|
t = self._song.tracks[int(track_index)]
|
|
|
|
# Intentar crear en Arrangement View primero
|
|
if arrangement:
|
|
arr_clips = getattr(t, "arrangement_clips", None)
|
|
if arr_clips is not None:
|
|
try:
|
|
beats_per_bar = int(self._song.signature_numerator)
|
|
start_beat = start_time * beats_per_bar
|
|
end_beat = start_beat + 4.0 * beats_per_bar
|
|
|
|
# Live 12+ API
|
|
new_clip = arr_clips.add_new_clip(start_beat, end_beat)
|
|
if new_clip and notes:
|
|
new_clip.set_notes(tuple(live_notes))
|
|
return {
|
|
"created": True,
|
|
"track_index": track_index,
|
|
"start_time": start_time,
|
|
"notes_added": len(notes),
|
|
"view": "arrangement" # ← EXPLÍCITO
|
|
}
|
|
except Exception:
|
|
pass # Fallback a Session
|
|
|
|
# Fallback: Session View (comportamiento actual)
|
|
slot = t.clip_slots[int(clip_index)]
|
|
slot.create_clip(4.0)
|
|
# ... resto del código
|
|
return {
|
|
"created": True,
|
|
"view": "session", # ← EXPLÍCITO
|
|
"note": "Clip created in Session View. Use fire_clip + record_to_arrangement to capture."
|
|
}
|
|
```
|
|
|
|
#### Opción B: Grabación Automática (produce_with_library)
|
|
|
|
En `_cmd_produce_with_library()`, después de crear todos los clips:
|
|
|
|
```python
|
|
def _cmd_produce_with_library(self, genre="reggaeton", tempo=95, ...):
|
|
# ... crear tracks y clips en Session View ...
|
|
|
|
# GRABAR AUTOMÁTICAMENTE A ARRANGEMENT
|
|
if record_arrangement:
|
|
self._enable_arrangement_overdub()
|
|
self._song.current_song_time = 0.0
|
|
|
|
# Disparar todos los clips
|
|
for track in tracks_creados:
|
|
if track.clip_slots[0].has_clip:
|
|
track.clip_slots[0].fire()
|
|
|
|
# Iniciar grabación
|
|
self._song.start_playing()
|
|
|
|
# Detener después de bars
|
|
import threading, time
|
|
def stop_after():
|
|
time.sleep(bars * 4 * 60.0 / tempo)
|
|
self._song.stop_playing()
|
|
self._song.arrangement_overdub = False
|
|
# Cambiar a Arrangement View
|
|
app = self._get_app()
|
|
if app:
|
|
app.view.show_view("Arranger")
|
|
|
|
threading.Thread(target=stop_after, daemon=True).start()
|
|
```
|
|
|
|
#### Opción C: Cambiar a Session View (mostrar al usuario)
|
|
|
|
Después de crear clips, forzar Ableton a mostrar Session View:
|
|
|
|
```python
|
|
def _cmd_generate_midi_clip(self, track_index, clip_index, notes, **kw):
|
|
# ... crear clip ...
|
|
|
|
# CAMBIAR A SESSION VIEW para que sea visible
|
|
app = self._get_app()
|
|
if app and hasattr(app, "view"):
|
|
app.view.show_view("Session")
|
|
|
|
return {"created": True, "view": "session"}
|
|
```
|
|
|
|
---
|
|
|
|
## PROBLEMA #2: `produce_with_library` Reporta 0 Samples
|
|
|
|
### 🟡 ALTO - Pipeline de producción incompleto
|
|
|
|
**Estado Actual**:
|
|
- ✅ Pipeline ejecuta sin errores
|
|
- ❌ **0 samples cargados de la librería**
|
|
- ❌ Tracks creados pero vacíos
|
|
|
|
### Análisis Técnico
|
|
|
|
**Handler**: `_cmd_produce_with_library()` (líneas 3,879-3,980)
|
|
|
|
Flujo de ejecución:
|
|
```
|
|
1. produce_with_library()
|
|
↓
|
|
2. Llama _cmd_load_samples_for_genre()
|
|
↓
|
|
3. SampleSelector.select_for_genre() retorna objeto 'group'
|
|
↓
|
|
4. Intenta acceder a: group.drums.kick, group.drums.snare, etc.
|
|
↓
|
|
5. Si group.drums es None → CONTINUE (skip silencioso)
|
|
↓
|
|
6. Resultado: 0 tracks creados, 0 samples cargados
|
|
```
|
|
|
|
**Causas posibles**:
|
|
1. **Import de SampleSelector falla** (línea 1,608) - Si hay error, continúa con `group = None`
|
|
2. **`group.drums` es None** - Todos los drums fallan
|
|
3. **Paths de samples no existen** - Verificación `os.path.isfile()` falla
|
|
4. **`group.bass`, `group.synths`, `group.fx` son None o vacíos**
|
|
|
|
### Código Problemático
|
|
|
|
```python
|
|
def _cmd_load_samples_for_genre(self, genre, key="", bpm=0, ...):
|
|
try:
|
|
from engines.sample_selector import SampleSelector
|
|
selector = SampleSelector()
|
|
group = selector.select_for_genre(str(genre), str(key) if key else None, ...)
|
|
except Exception as e:
|
|
self.log_message("T008 selector error: %s" % str(e))
|
|
return {"error": "SampleSelector failed: %s" % str(e)} # ← Retorna error
|
|
|
|
# ... si hay error arriba, nunca llega aquí ...
|
|
|
|
drum_map = [
|
|
("Kick", getattr(group.drums, "kick", None), 36), # ← Si group.drums es None → None
|
|
("Snare", getattr(group.drums, "snare", None), 38), # ← Todos fallan
|
|
# ...
|
|
]
|
|
for name, info, pad in drum_map:
|
|
if info is None or not os.path.isfile(info.path): # ← SKIP si None
|
|
continue # ← SILENCIOSO
|
|
```
|
|
|
|
### Solución Propuesta
|
|
|
|
#### Fix: Agregar validación y fallback
|
|
|
|
```python
|
|
def _cmd_produce_with_library(self, genre="reggaeton", tempo=95, ...):
|
|
# ...
|
|
sample_result = self._cmd_load_samples_for_genre(genre=genre, key=key, bpm=float(tempo))
|
|
|
|
# AGREGAR: Validación de error
|
|
if sample_result.get("error"):
|
|
# FALLBACK: Usar get_recommended_samples
|
|
try:
|
|
from engines.sample_selector import SampleSelector
|
|
selector = SampleSelector()
|
|
|
|
# Cargar manualmente con get_recommended_samples
|
|
drum_samples = selector.get_recommended_samples("drums", count=4)
|
|
bass_samples = selector.get_recommended_samples("bass", count=2)
|
|
|
|
for sample_info in drum_samples:
|
|
# Crear track y cargar
|
|
self._song.create_audio_track(-1)
|
|
idx = len(self._song.tracks) - 1
|
|
t = self._song.tracks[idx]
|
|
t.name = sample_info.role
|
|
self._cmd_load_sample_direct(idx, sample_info.path, auto_fire=True)
|
|
|
|
steps.append("Fallback: loaded %d samples via get_recommended_samples" % len(drum_samples))
|
|
except Exception as fallback_err:
|
|
steps.append("CRITICAL: Both methods failed: %s" % str(fallback_err))
|
|
else:
|
|
steps.append("library: %d tracks, %d samples loaded" % (
|
|
sample_result.get("tracks_created", 0),
|
|
sample_result.get("samples_loaded", 0),
|
|
))
|
|
|
|
# AGREGAR: Warning si 0 samples
|
|
if sample_result.get("samples_loaded", 0) == 0:
|
|
steps.append("WARNING: No samples loaded. Check library path: %s" % selector._library)
|
|
```
|
|
|
|
#### Fix: Debug logging en SampleSelector
|
|
|
|
```python
|
|
def _cmd_load_samples_for_genre(self, genre, key="", bpm=0, ...):
|
|
# ...
|
|
group = selector.select_for_genre(str(genre), ...)
|
|
|
|
# AGREGAR: Debug
|
|
self.log_message("SampleSelector returned group: %s" % str(group))
|
|
if group:
|
|
self.log_message("group.drums: %s" % str(getattr(group, 'drums', None)))
|
|
self.log_message("group.bass: %s" % str(getattr(group, 'bass', None)))
|
|
|
|
# ... resto del código
|
|
```
|
|
|
|
---
|
|
|
|
## PROBLEMA #3: Handlers con Nombres Engañosos
|
|
|
|
### 🟡 ALTO - Documentación incorrecta
|
|
|
|
**Problema**: Handlers con "arrangement" en el nombre que NO crean en Arrangement View.
|
|
|
|
### Lista de Handlers Afectados
|
|
|
|
| Handler | Líneas | Nombre Sugerido | Problema |
|
|
|---------|--------|-----------------|----------|
|
|
| `_cmd_create_arrangement_midi_clip` | 841-932 | `create_midi_clip_with_fallback` | Intenta Arrangement, fallback a Session |
|
|
| `_cmd_create_arrangement_audio_pattern` | 553-575 | `create_audio_pattern_session` | Solo crea en Session (slot 0) |
|
|
| `_cmd_duplicate_session_to_arrangement` | 751-777 | `fire_session_clips` | Solo hace fire, no duplica |
|
|
| `_cmd_record_to_arrangement` | 3713-3775 | `fire_and_record_session` | Activa overdub pero no garantiza grabación |
|
|
|
|
### Solución Propuesta
|
|
|
|
#### Opción A: Renombrar handlers para reflejar comportamiento real
|
|
|
|
```python
|
|
# Antes
|
|
def _cmd_create_arrangement_midi_clip(self, ...): # Engañoso
|
|
|
|
# Después
|
|
def _cmd_create_midi_clip_arrangement_or_session(self, ...): # Claro
|
|
"""Create MIDI clip - attempts Arrangement, falls back to Session View."""
|
|
```
|
|
|
|
#### Opción B: Implementar comportamiento real de Arrangement
|
|
|
|
Para `_cmd_record_to_arrangement()`:
|
|
|
|
```python
|
|
def _cmd_record_to_arrangement_fixed(self, duration_bars=8, **kw):
|
|
"""ACTUALMENTE: Activa overdub y dispara clips
|
|
NECESITA: Scheduler real que capture a Arrangement"""
|
|
|
|
# Usar el scheduler ya implementado en build_song (líneas 4314-4403)
|
|
return self._cmd_build_song(bpm=self._song.tempo, key="Am",
|
|
record_duration=duration_bars,
|
|
only_record=True)
|
|
```
|
|
|
|
---
|
|
|
|
## PROBLEMA #4: Race Condition en Dispatch
|
|
|
|
### 🟠 MEDIO - Tareas pueden no ejecutarse inmediatamente
|
|
|
|
### Análisis Técnico
|
|
|
|
**Arquitectura de Threads**:
|
|
```
|
|
MCP Server Thread Ableton Live UI Thread (Main)
|
|
| |
|
|
|── _dispatch() |── update_display() [~100ms]
|
|
| └── añade task | └── ejecuta task()
|
|
| a _pending_tasks[] |
|
|
| |
|
|
└── q.get(timeout=30s) ←───────┘
|
|
↑
|
|
└── espera resultado
|
|
```
|
|
|
|
**Problema**: El cliente MCP espera el resultado vía `q.get(timeout=30s)`, pero la tarea solo se ejecuta cuando Live llama `update_display()` (cada ~100ms).
|
|
|
|
Si Live está ocupado o en background, `update_display()` puede tardar más, causando timeout.
|
|
|
|
### Solución Propuesta
|
|
|
|
#### Opción A: Timeout más corto + retry
|
|
|
|
```python
|
|
def _dispatch(self, cmd):
|
|
# ... añadir task a cola ...
|
|
|
|
# Reducir timeout de 30s a 5s
|
|
try:
|
|
resp = q.get(timeout=5.0)
|
|
except _queue.Empty:
|
|
# Intentar ejecutar directamente como fallback
|
|
try:
|
|
result = task() # Ejecutar ahora
|
|
return {"status": "success", "result": result}
|
|
except Exception as e:
|
|
return {"status": "error", "message": "Timeout and direct execution failed: %s" % str(e)}
|
|
```
|
|
|
|
#### Opción B: Health check de update_display
|
|
|
|
```python
|
|
def update_display(self):
|
|
self._last_update_time = time.time() # Registrar
|
|
# ... resto del código
|
|
|
|
# Nuevo comando MCP
|
|
def _cmd_health_check_dispatch(self):
|
|
last = getattr(self, '_last_update_time', 0)
|
|
elapsed = time.time() - last
|
|
if elapsed > 5.0: # No se llamó en 5 segundos
|
|
return {"healthy": False, "issue": "update_display not called in %ds" % elapsed}
|
|
return {"healthy": True, "last_update_ms": int(elapsed * 1000)}
|
|
```
|
|
|
|
---
|
|
|
|
## PROBLEMA #5: Inconsistencias de Reporte
|
|
|
|
### 🟠 MEDIO - Diferentes tools reportan diferentes datos
|
|
|
|
### Inconsistencias Encontradas
|
|
|
|
| Tool | Tracks Reportados | Estado |
|
|
|------|-------------------|--------|
|
|
| `get_tracks()` | 4 | ✅ Correcto |
|
|
| `get_project_summary()` | 0 | ❌ Incorrecto |
|
|
| `validate_project()` | "proyecto sin tracks" | ❌ Incorrecto |
|
|
| `full_quality_check()` | 4 tracks vacíos | ✅ Correcto |
|
|
| `get_workflow_status()` | 4 tracks con nombres | ✅ Correcto |
|
|
|
|
### Causa Técnica
|
|
|
|
`get_project_summary()` no está iterando sobre `self._song.tracks` correctamente:
|
|
|
|
```python
|
|
def _cmd_get_project_summary(self):
|
|
# PROBLEMA: Esto retorna 0
|
|
track_count = len([t for t in self._song.tracks if t.is_visible]) # ← is_visible?
|
|
|
|
# CORRECCIÓN: Debería ser
|
|
track_count = len(self._song.tracks) # Todos los tracks
|
|
```
|
|
|
|
### Solución
|
|
|
|
```python
|
|
def _cmd_get_project_summary(self):
|
|
tracks = list(self._song.tracks) # Convertir a lista explícita
|
|
midi_tracks = [t for t in tracks if hasattr(t, 'has_midi_input') and t.has_midi_input]
|
|
audio_tracks = [t for t in tracks if hasattr(t, 'has_audio_input') and t.has_audio_input]
|
|
|
|
return {
|
|
"track_count": len(tracks), # ← CORREGIDO
|
|
"midi_tracks": len(midi_tracks),
|
|
"audio_tracks": len(audio_tracks),
|
|
# ... resto
|
|
}
|
|
```
|
|
|
|
---
|
|
|
|
## PRIORIDADES DE FIX
|
|
|
|
### 🔴 URGENTE (Bloquea producción)
|
|
|
|
1. **Agregar parámetro `arrangement=True`** a `generate_midi_clip()` y `load_sample_direct()`
|
|
2. **Implementar grabación real** en `record_to_arrangement()` usando el scheduler de `build_song`
|
|
3. **Fix `produce_with_library`** para usar `get_recommended_samples()` como fallback
|
|
|
|
### 🟡 ALTO (Mejora UX)
|
|
|
|
4. **Renombrar handlers** o agregar documentación clara sobre Session vs Arrangement
|
|
5. **Corregir `get_project_summary()`** para reportar tracks correctamente
|
|
6. **Agregar debug logging** en SampleSelector para diagnóstico
|
|
|
|
### 🟢 MEDIO (Optimización)
|
|
|
|
7. **Reducir timeout** en dispatch de 30s a 5s
|
|
8. **Agregar health check** de update_display
|
|
9. **Optimizar** cola de pending_tasks
|
|
|
|
---
|
|
|
|
## FLUJO RECOMENDADO POST-FIX
|
|
|
|
### Para Usuario:
|
|
|
|
```python
|
|
# 1. Setup
|
|
/set_tempo 95
|
|
/set_time_signature 4 4
|
|
|
|
# 2. Producción con Arrangement View explícito
|
|
/produce_with_library genre=reggaeton key=Am tempo=95 bars=16 record_arrangement=true
|
|
|
|
# 3. Si produce_with_library falla, modo manual:
|
|
/scan_library subfolder=reggaeton/kick
|
|
/load_sample_direct track=2 file=.../kick 1.wav arrangement=true start_time=0
|
|
/generate_midi_clip track=0 notes=[...] arrangement=true start_time=0
|
|
|
|
# 4. Verificar en Arrangement View
|
|
/show_arrangement_view # Cambia la vista
|
|
/get_arrangement_clips # Lista clips en Arrangement
|
|
```
|
|
|
|
---
|
|
|
|
## ARCHIVOS DE REFERENCIA
|
|
|
|
- **Archivo principal**: `AbletonMCP_AI/__init__.py` (4,428 líneas)
|
|
- **Handlers críticos**: Líneas 553-932 (Arrangement), 1,816-1,860 (MIDI), 3,822-3,980 (Samples)
|
|
- **Scheduler de grabación**: Líneas 4,314-4,403 (`build_song`)
|
|
|
|
---
|
|
|
|
**Generado por**: 5 agentes paralelos (Kimi K2)
|
|
**Fecha**: 2026-04-11
|
|
**Para**: Qwen (Review/Implementation)
|
|
**Status**: Listo para Sprint de Fixes
|