Files
ableton-mcp-ai/docs/ANALISIS_CRITICO_SPRINT_4.md
OpenCode Agent 5ce8187c65 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
2026-04-12 14:02:32 -03:00

16 KiB

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)

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)

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:

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:

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:

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

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

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

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

# 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():

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

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

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:

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

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)

  1. Renombrar handlers o agregar documentación clara sobre Session vs Arrangement
  2. Corregir get_project_summary() para reportar tracks correctamente
  3. Agregar debug logging en SampleSelector para diagnóstico

🟢 MEDIO (Optimización)

  1. Reducir timeout en dispatch de 30s a 5s
  2. Agregar health check de update_display
  3. Optimizar cola de pending_tasks

FLUJO RECOMENDADO POST-FIX

Para Usuario:

# 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