feat: Implement senior audio injection with 5 fallback methods
- 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
This commit is contained in:
493
docs/ANALISIS_CRITICO_SPRINT_4.md
Normal file
493
docs/ANALISIS_CRITICO_SPRINT_4.md
Normal file
@@ -0,0 +1,493 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user