# SPRINT 8 — FIX: ESPACIADO DE CLIPS EN ARRANGEMENT VIEW (T001-T030)

> **Fecha**: 2026-04-13
> **Autor**: Antigravity (análisis) → para implementación por **Kimi K2.5**
> **Reviewer**: Qwen (compilar + verificar)
> **Problema reportado**: El sistema crea música pero todos los clips quedan pegados entre sí, sin espacios (gaps) en el Arrangement View.

---

## 🔴 DIAGNÓSTICO RAÍZ (5 causas identificadas)

### Causa 1 — `build_song` usa Session View + recording overdub (CRÍTICO)

**Archivo**: `AbletonMCP_AI/__init__.py`, líneas ~6256-6435

`_cmd_build_song` coloca clips en `clip_slots[row]` (Session View), y luego llama a `_schedule_arrangement_recording`. El scheduler:
1. Hace `fire_scene(row)` → la escena toca
2. Espera `duration_sec = bars * (60/tempo) * 4`
3. **No hay pausa entre secciones** → la siguiente escena se dispara inmediatamente después

**Resultado**: En Arrangement View, los clips quedan uno pegado al otro sin ningún gap.

```python
# CÓDIGO PROBLEMÁTICO (línea 6514):
duration_sec = bars * (60.0 / tempo) * 4.0
st["section_end_time"] = time.time() + duration_sec
st["phase"] = "waiting"
# cuando expira, inmediatamente dispara la SIGUIENTE escena sin gap
```

---

### Causa 2 — `produce_13_scenes` hace lo mismo (CRÍTICO)

**Archivo**: `AbletonMCP_AI/__init__.py`, líneas ~6817-6823

```python
if record_arrangement:
    sections_for_recording = []
    for scene_name, duration, energy, flags in self.SCENES:
        sections_for_recording.append((scene_name, 0, duration, flags))
    self._schedule_arrangement_recording(sections_for_recording)
```

Pasa `row=0` para **todos** los scenes → `fire_scene(0)` siempre dispara la primera escena.
No hay gap entre secciones.

---

### Causa 3 — `_arr_record_tick` no espera quantización de bar (MEDIO)

Al terminar una sección, el tick avanza inmediatamente al siguiente sin esperar el downbeat del siguiente compás. Causa micro-overlaps de milisegundos visibles en la Timeline.

---

### Causa 4 — `_cmd_create_arrangement_audio_pattern` ignora `gap_bars` (MEDIO)

La función acepta `positions` (lista de beats donde colocar clips), pero cuando el caller solo pasa `[0]`, todos los clips de diferentes tracks quedan en beat 0.

---

### Causa 5 — `_get_audio_duration_beats` hace cap a 64 beats (MENOR)

```python
return min(duration_beats, 16.0 * beats_per_bar)  # cap a 64 beats
```

Si el sample dura más de 64 beats, el cap hace que el siguiente clip solape o quede muy cerca del anterior.

---

## ✅ PLAN DE FIXES (T001-T030)

### FASE 1: FIX CRÍTICO — GAP ENTRE SECCIONES EN SCHEDULER (T001-T005)

**T001** — Agregar parámetro `gap_bars` a `_schedule_arrangement_recording`:

Ubicación: `__init__.py`, línea ~6459

```python
# ANTES:
def _schedule_arrangement_recording(self, sections):
    self._song.current_song_time = 0.0
    if hasattr(self._song, "arrangement_overdub"):
        self._song.arrangement_overdub = True
    self._arr_record_state = {
        "sections": sections,
        "idx": 0,
        "phase": "start",
        "section_end_time": 0.0,
        "done": False,
    }

# DESPUÉS:
def _schedule_arrangement_recording(self, sections, gap_bars=2.0):
    """
    gap_bars: número de compases de silencio ENTRE secciones.
               Default = 2 (suficiente para escuchar cada sección separada).
               Usar 0 para pegado (comportamiento anterior).
    """
    self._song.current_song_time = 0.0
    if hasattr(self._song, "arrangement_overdub"):
        self._song.arrangement_overdub = True

    self._arr_record_state = {
        "sections": sections,
        "idx": 0,
        "phase": "start",
        "section_end_time": 0.0,
        "done": False,
        "gap_bars": float(gap_bars),    # ← NUEVO
        "gap_end_time": 0.0,            # ← NUEVO
    }
```

---

**T002** — Modificar `_arr_record_tick` para insertar gap entre secciones:

Ubicación: `__init__.py`, línea ~6518

```python
# ANTES:
elif phase == "waiting":
    if time.time() >= st["section_end_time"]:
        # This section is done — move to next
        st["idx"] += 1
        if st["idx"] < len(st["sections"]):
            st["phase"] = "start"
        else:
            self._arr_record_finish(st)

# DESPUÉS:
elif phase == "waiting":
    if time.time() >= st["section_end_time"]:
        # Parar todos los clips antes del gap
        try:
            self._song.stop_all_clips()
        except Exception:
            pass

        gap_bars = st.get("gap_bars", 2.0)
        if gap_bars > 0:
            # Mantener transport corriendo durante el gap (para grabar silencio)
            if not self._song.is_playing:
                self._song.start_playing()
            tempo = float(self._song.tempo)
            gap_sec = gap_bars * (60.0 / tempo) * 4.0
            st["phase"] = "gap"
            st["gap_end_time"] = time.time() + gap_sec
            self.log_message("AbletonMCP_AI: Gap: %.1f bars (%.1fs)" % (gap_bars, gap_sec))
        else:
            # Sin gap: comportamiento anterior
            st["idx"] += 1
            if st["idx"] < len(st["sections"]):
                st["phase"] = "start"
            else:
                self._arr_record_finish(st)

# AGREGAR nuevo bloque elif para fase "gap" DENTRO del mismo método,
# después del bloque "waiting":
elif phase == "gap":
    if time.time() >= st.get("gap_end_time", 0):
        st["idx"] += 1
        if st["idx"] < len(st["sections"]):
            st["phase"] = "start"
        else:
            self._arr_record_finish(st)
```

---

**T003** — Actualizar `_cmd_build_song` para pasar `gap_bars`:

Ubicación: `__init__.py`, línea ~6434

```python
# ANTES:
if auto_record:
    self._schedule_arrangement_recording(sections)
    log.append("arrangement recording started (%d sections)" % len(sections))

# DESPUÉS:
if auto_record:
    gap_bars = float(kw.get("gap_bars", 2.0))
    self._schedule_arrangement_recording(sections, gap_bars=gap_bars)
    log.append("arrangement recording started (%d sections, gap=%.1f bars)" % (len(sections), gap_bars))
```

También agregar `gap_bars=2.0` al signature del método:
```python
# ANTES:
def _cmd_build_song(self, genre="reggaeton", tempo=95, key="Am",
                    style="standard", auto_record=True, **kw):

# DESPUÉS:
def _cmd_build_song(self, genre="reggaeton", tempo=95, key="Am",
                    style="standard", auto_record=True, gap_bars=2.0, **kw):
```

---

**T004** — Actualizar `_cmd_produce_13_scenes` para pasar `row` correcto y `gap_bars`:

Ubicación: `__init__.py`, línea ~6817

```python
# ANTES:
if record_arrangement:
    sections_for_recording = []
    for scene_name, duration, energy, flags in self.SCENES:
        sections_for_recording.append((scene_name, 0, duration, flags))
    self._schedule_arrangement_recording(sections_for_recording)
    log.append("Arrangement recording scheduled")

# DESPUÉS:
if record_arrangement:
    sections_for_recording = []
    for si, (scene_name, duration, energy, flags) in enumerate(self.SCENES):
        sections_for_recording.append((scene_name, si, duration, flags))  # row = si
    gap_bars_val = float(kw.get("gap_bars", 2.0))
    self._schedule_arrangement_recording(sections_for_recording, gap_bars=gap_bars_val)
    log.append("Arrangement recording scheduled (%d scenes, gap=%.1f bars)" % (
        len(sections_for_recording), gap_bars_val))
```

También agregar `gap_bars=2.0` al signature:
```python
def _cmd_produce_13_scenes(self, genre="reggaeton", tempo=95, key="Am",
                            auto_play=True, record_arrangement=True,
                            force_bpm_coherence=True, gap_bars=2.0, **kw):
```

---

**T005** — Actualizar `_cmd_get_recording_status` para reportar estado del gap:

Ubicación: `__init__.py`, línea ~6550

```python
# En el return de _cmd_get_recording_status, agregar:
return {
    "recording": True,
    "done": st.get("done", False),
    "section_index": idx,
    "section_name": name,
    "phase": phase,           # Ahora puede ser "start"|"waiting"|"gap"|"done"
    "sections_total": len(sections),
    "section_remaining_seconds": remaining,
    "gap_bars": st.get("gap_bars", 2.0),           # ← NUEVO
    "gap_remaining_seconds": max(                   # ← NUEVO
        0.0,
        round(st.get("gap_end_time", 0) - time.time(), 1)
    ) if phase == "gap" else 0.0,
}
```

---

### FASE 2: FIX MEDIO — QUANTIZACIÓN AL BAR (T006-T010)

**T006** — Logging de posición en bars al iniciar cada sección:

En `_arr_record_tick`, fase `"start"`, justo después del `fire_scene`:

```python
# Agregar após fire_scene (línea ~6506):
try:
    beats_pos = float(self._song.current_song_time)
    beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
    bars_pos = beats_pos / beats_per_bar if beats_per_bar > 0 else 0.0
    self.log_message("AbletonMCP_AI: Recording %d/%d: %s (%d bars) @ bar %.1f" % (
        idx + 1, len(sections), name, bars, bars_pos))
except Exception:
    pass
```

**T007** — Verificar que `stop_all_clips` no corta el transport:

Agregar después de `stop_all_clips()` en la fase waiting→gap:

```python
# Asegurar que el transport siga corriendo para grabar el silencio
if not self._song.is_playing:
    try:
        self._song.start_playing()
    except Exception:
        pass
```

**T008** — Agregar parámetro `quantize=True` a `_schedule_arrangement_recording`:

```python
def _schedule_arrangement_recording(self, sections, gap_bars=2.0, quantize=True):
    ...
    self._arr_record_state = {
        ...
        "gap_bars": float(gap_bars),
        "quantize": bool(quantize),
    }
```

**T009** — En fase `"gap"`, si `quantize=True`, esperar el siguiente downbeat:

```python
elif phase == "gap":
    if time.time() >= st.get("gap_end_time", 0):
        # Si quantize, esperar al siguiente bar boundary
        quantize = st.get("quantize", True)
        if quantize:
            try:
                beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
                current_beat = float(self._song.current_song_time)
                # Calcular si estamos en un downbeat (±0.1 beats tolerancia)
                beat_in_bar = current_beat % beats_per_bar
                at_downbeat = beat_in_bar < 0.2 or beat_in_bar > (beats_per_bar - 0.2)
                if not at_downbeat:
                    # No al downbeat aún, seguir esperando
                    return
            except Exception:
                pass
        
        st["idx"] += 1
        if st["idx"] < len(st["sections"]):
            st["phase"] = "start"
        else:
            self._arr_record_finish(st)
```

**T010** — Compilar y test básico de scheduler con `gap_bars=2`:

```powershell
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\__init__.py"
```

Verificar `get_recording_status()` → `"phase": "gap"` aparece entre secciones.

---

### FASE 3: FIX — PLACEMENT DIRECTO EN ARRANGEMENT (T011-T020)

**T011** — Crear helper `_bars_to_beats` y `_beats_to_bars`:

```python
def _bars_to_beats(self, bars):
    """Convertir bars a beats usando la firma de tiempo actual."""
    beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
    return float(bars) * beats_per_bar

def _beats_to_bars(self, beats):
    """Convertir beats a bars usando la firma de tiempo actual."""
    beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
    return float(beats) / beats_per_bar if beats_per_bar > 0 else 0.0
```

**T012** — Crear `_cmd_build_song_arrangement` (nuevo handler, NO modifica el viejo):

```python
def _cmd_build_song_arrangement(self, genre="reggaeton", tempo=95, key="Am",
                                 style="standard", gap_bars=2.0, **kw):
    """BUILD_SONG v2 — Coloca clips DIRECTAMENTE en Arrangement View.
    
    NO usa Session View. NO usa overdub recording.
    Calcula start_bar acumulativo con gap entre secciones.
    
    Args:
        genre: Género musical
        tempo: BPM
        key: Tonalidad (Am, C, F, etc.)
        style: Estilo del patrón
        gap_bars: Compases de silencio entre secciones (default 2.0)
    """
    import os
    log = []
    
    SCRIPT = os.path.dirname(os.path.abspath(__file__))
    LIB = os.path.normpath(os.path.join(SCRIPT, "..", "libreria", "reggaeton"))
    
    self._song.tempo = float(tempo)
    beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
    gap_bars = float(gap_bars)
    
    # Estructura de secciones
    bars_intro  = 4
    bars_verse  = 8
    bars_chorus = 8
    bars_bridge = 4
    bars_outro  = 4
    
    sections_def = [
        ("Intro",  bars_intro,  {"sparse": True,  "full": False}),
        ("Verse",  bars_verse,  {"sparse": False, "full": False}),
        ("Chorus", bars_chorus, {"sparse": False, "full": True}),
        ("Bridge", bars_bridge, {"sparse": True,  "full": False}),
        ("Outro",  bars_outro,  {"sparse": True,  "full": False}),
    ]
    
    # Calcular posiciones acumulativas con gap
    current_bar = 0.0
    sections_with_pos = []
    for name, dur, opts in sections_def:
        sections_with_pos.append((name, current_bar, dur, opts))
        current_bar += dur + gap_bars
    
    # Seleccionar samples
    def _pick(subfolder, n=2):
        d = os.path.join(LIB, subfolder)
        if not os.path.isdir(d):
            return []
        files = sorted([f for f in os.listdir(d)
                        if f.lower().endswith(('.wav', '.aif', '.aiff', '.mp3'))])
        return [os.path.join(d, files[i % len(files)]) for i in range(n)] if files else []
    
    kicks   = _pick("kick", 2)
    snares  = _pick("snare", 2)
    hats    = _pick("hi-hat (para percs normalmente)", 2)
    bass    = _pick("bass", 2)
    loops   = _pick("drumloops", 2)
    percs   = _pick("perc loop", 2)
    
    # Crear tracks
    self._song.create_audio_track(-1); drum_loop_idx = len(self._song.tracks) - 1
    self._song.tracks[drum_loop_idx].name = "Drum Loop"
    self._song.create_audio_track(-1); kick_idx = len(self._song.tracks) - 1
    self._song.tracks[kick_idx].name = "Kick"
    self._song.create_audio_track(-1); snare_idx = len(self._song.tracks) - 1
    self._song.tracks[snare_idx].name = "Snare"
    self._song.create_midi_track(-1); dembow_idx = len(self._song.tracks) - 1
    self._song.tracks[dembow_idx].name = "Dembow"
    
    # Colocar clips con posiciones correctas
    clips_created = 0
    for si, (sec_name, start_bar, dur_bars, opts) in enumerate(sections_with_pos):
        log.append("Section: %s @ bar %.1f (dur=%.1f)" % (sec_name, start_bar, dur_bars))
        
        # Audio clips
        if loops and not opts.get("sparse"):
            result = self._cmd_create_arrangement_audio_pattern(
                track_index=drum_loop_idx,
                file_path=loops[si % len(loops)],
                positions=[start_bar],
                name=sec_name + "_loop"
            )
            if result.get("positions_created"):
                clips_created += 1
        
        if kicks and not opts.get("sparse"):
            result = self._cmd_create_arrangement_audio_pattern(
                track_index=kick_idx,
                file_path=kicks[si % len(kicks)],
                positions=[start_bar],
                name=sec_name + "_kick"
            )
            if result.get("positions_created"):
                clips_created += 1
        
        if snares and not opts.get("sparse"):
            result = self._cmd_create_arrangement_audio_pattern(
                track_index=snare_idx,
                file_path=snares[si % len(snares)],
                positions=[start_bar],
                name=sec_name + "_snare"
            )
            if result.get("positions_created"):
                clips_created += 1
        
        # MIDI clips en Arrangement
        start_beat = self._bars_to_beats(start_bar)
        length_beats = self._bars_to_beats(dur_bars)
        
        if not opts.get("sparse"):
            try:
                variation = "double" if opts.get("full") else "standard"
                dembow_notes = self._generate_dembow_notes_raw(
                    bars=dur_bars, variation=variation
                )
                self._cmd_create_arrangement_midi_clip(
                    track_index=dembow_idx,
                    start_time=start_beat,
                    length=length_beats,
                    notes=dembow_notes,
                    name=sec_name + "_dembow"
                )
                clips_created += 1
            except Exception as e:
                log.append("dembow %s: %s" % (sec_name, str(e)))
    
    # Mostrar Arrangement View
    try:
        app = self._get_app()
        if app and hasattr(app, "view"):
            app.view.show_view("Arranger")
    except Exception:
        pass
    
    return {
        "built": True,
        "method": "direct_arrangement",
        "genre": genre,
        "tempo": float(self._song.tempo),
        "key": key,
        "sections": len(sections_with_pos),
        "clips_created": clips_created,
        "gap_bars": gap_bars,
        "total_bars": current_bar - gap_bars,  # total sin el último gap
        "log": log
    }
```

**T013** — Crear helper `_generate_dembow_notes_raw(bars, variation)`:

Extraer la lógica de generación de notas del dembow de `_cmd_generate_dembow_clip` a un helper que solo devuelva la lista de notas sin tocar Ableton.

```python
def _generate_dembow_notes_raw(self, bars=4, variation="standard"):
    """Generar notas de patrón dembow sin crear clips. Retorna lista de dicts.
    
    Returns:
        List of {"pitch": int, "start_time": float, "duration": float, "velocity": int}
    """
    # ... copiar/refactorizar la lógica existente de _cmd_generate_dembow_clip ...
    # El método existente ya genera las notas; solo necesitamos el raw output
    notes = []
    # [Lógica de generación de dembow aquí - copiar de _cmd_generate_dembow_clip]
    return notes
```

**T014** — Crear tool MCP `build_song_arrangement` en `server.py`:

```python
@mcp.tool()
def build_song_arrangement(
    genre: str = "reggaeton",
    tempo: float = 95,
    key: str = "Am",
    style: str = "standard",
    gap_bars: float = 2.0
) -> dict:
    """Build complete song with proper spacing between sections in Arrangement View.
    
    Coloca clips DIRECTAMENTE en Arrangement View (sin Session intermediate).
    
    Args:
        genre: Music genre (reggaeton, trap, etc.)
        tempo: BPM
        key: Musical key (Am, C, F, etc.)
        style: Pattern style (standard, minimal, full)
        gap_bars: Bars of silence between sections (default 2.0, use 0 for no gap)
    
    Returns:
        Dict with sections created, clips placed, and timeline positions
    """
    return _send("build_song_arrangement", {
        "genre": genre,
        "tempo": tempo,
        "key": key,
        "style": style,
        "gap_bars": gap_bars
    })
```

**T015** — Agregar `gap_bars` a tool MCP `produce_13_scenes` en `server.py`:

Buscar `def produce_13_scenes` en `server.py` y agregar parámetro:

```python
# Agregar al signature:
gap_bars: float = 2.0

# Agregar al dict del _send():
"gap_bars": gap_bars
```

**T016** — Agregar `gap_bars` a tool MCP `build_song` en `server.py`:

Mismo que T015 pero para `build_song`.

**T017** — Verificar conversión bars→beats en `_cmd_create_arrangement_audio_pattern`:

Línea ~1252 de `__init__.py`:
```python
# Este código YA existe y es correcto — solo verificar:
beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
start_beat = position * beats_per_bar  # ← position es en BARS, correcto
```

Si esta línea NO existe o convierte mal, es un bug adicional que corregir.

**T018** — Documentar en docstring de `_cmd_create_arrangement_audio_pattern` que `positions` es en BARS:

```python
def _cmd_create_arrangement_audio_pattern(self, track_index, file_path, positions, name="", **kw):
    """Create one or more arrangement audio clips from an absolute file path.
    
    Args:
        track_index: Track index (0-based)
        file_path: Absolute path to audio file
        positions: List of bar positions (NOT beats) where clips will be placed.
                   e.g. [0, 8, 16] = clip at bar 0, 8, and 16.
                   Internally converted to beats: position * beats_per_bar
        name: Clip name prefix
    """
```

**T019** — Aumentar cap en `_get_audio_duration_beats`:

Línea ~1241:

```python
# ANTES:
return min(duration_beats, 16.0 * beats_per_bar)  # cap a 64 beats

# DESPUÉS:
MAX_CLIP_BEATS = 128.0  # 32 bars máx (suficiente para loops largos)
return min(duration_beats, MAX_CLIP_BEATS)
```

**T020** — VERIFICACIÓN: Llamar `get_arrangement_clips()` después de `build_song_arrangement()`:

```python
# Verificar que los clips tienen start_times separados:
# Esperado para gap_bars=2, tempo=95:
# - Intro:  start_time = 0.0  beats
# - Verse:  start_time = 24.0 beats (4 bars intro + 2 bars gap = 6 bars × 4 beats)
# - Chorus: start_time = 64.0 beats (6 + 8 + 2 = 16 bars × 4 beats)
# - Bridge: start_time = 96.0 beats (16 + 8 + 2 = 26 bars × 4 beats)
# - Outro:  start_time = 112.0 beats (26 + 4 + 2 = 32 bars × 4 beats)
```

---

### FASE 4: FIX — MIDI CLIP SPACING (T021-T025)

**T021** — En `_cmd_generate_dembow_clip`, verificar si se pasa `start_time` explícito:

```python
def _cmd_generate_dembow_clip(self, track_index, clip_index=0,
                               bars=4, variation="standard",
                               start_time=None,   # ← NUEVO: si se da, usar arrangement
                               **kw):
    """...
    Args:
        start_time: Si se especifica (en BEATS), crear en Arrangement View.
                    Si es None, crear en Session View en slot clip_index.
    """
    if start_time is not None:
        # Modo Arrangement: crear en posición específica
        notes = self._generate_dembow_notes_raw(bars=bars, variation=variation)
        beats_per_bar = float(getattr(self._song, 'signature_numerator', 4))
        length_beats = float(bars) * beats_per_bar
        return self._cmd_create_arrangement_midi_clip(
            track_index=track_index,
            start_time=float(start_time),
            length=length_beats,
            notes=notes
        )
    # Else: comportamiento anterior (Session View)
    ...
```

**T022** — Mismo patrón para `_cmd_generate_bass_clip`:

Igual que T021 pero para la función de bass.

**T023** — Mismo patrón para `_cmd_generate_chords_clip`:

Igual que T021 pero para chords.

**T024** — Mismo patrón para `_cmd_generate_melody_clip`:

Igual que T021 pero para melody.

**T025** — En `_cmd_build_song_arrangement`, usar el nuevo parámetro `start_time` para MIDI:

```python
# En el loop de secciones de _cmd_build_song_arrangement:
start_beat = self._bars_to_beats(start_bar)

# Dembow
self._cmd_generate_dembow_clip(
    dembow_idx,
    bars=dur_bars,
    variation=variation,
    start_time=start_beat   # ← modo arrangement
)

# Bass
self._cmd_generate_bass_clip(
    bass_idx,
    bars=dur_bars,
    key=root_key,
    start_time=start_beat   # ← modo arrangement
)
```

---

### FASE 5: VERIFICACIÓN Y DOCUMENTACIÓN (T026-T030)

**T026** — Compilar ambos archivos:

```powershell
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\__init__.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\mcp_server\server.py"
```

**T027** — Test básico con `build_song(gap_bars=4)`:

Verificar mediante `get_arrangement_clips()` que los clips tienen `start_time` separados ≥ 4 bars entre secciones.

```
Esperado (gap_bars=4, tempo=95, 4/4):
  Intro:  start 0 beats
  Verse:  start 32 beats (4+4=8 bars × 4 beats)
  Chorus: start 96 beats (8+8+4=20 bars × 4 beats)  
  Bridge: start 144 beats (20+8+4=32 bars × 4 beats)
  Outro:  start 160 beats (32+4+4=40 bars × 4 beats)
```

**T028** — Test de `get_recording_status()` durante recording:

Verificar que entre secciones aparece `"phase": "gap"` y `"gap_remaining_seconds"` decreciente.

**T029** — Actualizar `docs/ROADMAP_SPRINTS_AND_BUGS.md`:

- Marcar Sprint 8 con progreso
- Agregar bug: `B007 — Clips sin espacios en Arrangement (zero-gap)` → ✅ Fixed
- Actualizar métricas de sprint

**T030** — Actualizar `docs/GUIA_DE_USO.md` con parámetro `gap_bars`:

```markdown
## Parámetro `gap_bars` (nuevo en Sprint 8)

Todos los comandos de producción aceptan `gap_bars` (default 2.0):

| Valor | Resultado |
|-------|-----------|
| `gap_bars=0` | Clips pegados (comportamiento anterior) |
| `gap_bars=2` | 2 compases de silencio entre secciones (default) |
| `gap_bars=4` | 4 compases — recomendado para mezcla clara |
| `gap_bars=8` | 8 compases — útil para shows en vivo con transiciones largas |

### Ejemplo:
```python
build_song(tempo=95, key="Am", gap_bars=4)
produce_13_scenes(gap_bars=2)
build_song_arrangement(gap_bars=0)  # Sin gaps, direct placement
```
```

---

## 📁 ARCHIVOS A MODIFICAR

| Archivo | Cambios | Tareas |
|---------|---------|--------|
| `AbletonMCP_AI/__init__.py` | `_schedule_arrangement_recording` + `_arr_record_tick` + `_cmd_build_song` + `_cmd_produce_13_scenes` + nuevo `_cmd_build_song_arrangement` + helpers `_bars_to_beats`/`_beats_to_bars` + `_generate_dembow_notes_raw` + modo `start_time` en MIDI generators | T001-T005, T006-T010, T011-T013, T017-T025 |
| `mcp_server/server.py` | Tool `build_song_arrangement` (nueva) + `gap_bars` en `produce_13_scenes` y `build_song` | T014-T016 |
| `docs/ROADMAP_SPRINTS_AND_BUGS.md` | B007 fixed, sprint status | T029 |
| `docs/GUIA_DE_USO.md` | Documentar `gap_bars` | T030 |

---

## ⚠️ RESTRICCIONES

1. **Compilar después de CADA archivo modificado**
2. **NO tocar `libreria/`** — solo lectura
3. **Retrocompatibilidad**: `gap_bars=0` → comportamiento idéntico al anterior
4. **NO eliminar `_cmd_build_song` viejo** — solo agregar `gap_bars` con default
5. **Usar overwrite de archivos, NUNCA borrar+crear**
6. **Restart Ableton después de cambios a `__init__.py`**

---

## 🎯 CRITERIOS DE ACEPTACIÓN

- [ ] `build_song(gap_bars=4)` → clips separados ≥4 bars en Arrangement View
- [ ] `produce_13_scenes(gap_bars=2)` → 13 scenes con gaps visibles entre ellas
- [ ] `get_recording_status()` reporta `"phase": "gap"` durante silencios
- [ ] `build_song_arrangement()` coloca clips directamente sin Session intermediate
- [ ] Retrocompatibilidad: `build_song()` sin `gap_bars` funciona igual que antes
- [ ] Compilación 100% sin errores

---

## 📊 VISUALIZACIÓN DEL RESULTADO ESPERADO

### ANTES (bug — clips pegados):
```
Bar: 0    4    12   20   24   28
     [Intro][Verse][Chorus][Bridge][Outro]
     ↑ todos pegados, sin respiración
```

### DESPUÉS (fix — gap_bars=2):
```
Bar: 0    4  6    14  16    24  26    30  32    36
     [Intro]  [Verse]   [Chorus]  [Bridge]  [Outro]
           ↑        ↑         ↑         ↑
           2 bars de gap (silencio) entre cada sección
```

---

**Para Kimi K2.5:** Implementar en orden STRICT: Fase 1 → Compilar → Fase 2 → Compilar → etc.
**Para Qwen:** Verificar compilación + probar con Ableton abierto + confirmar gaps en Arrangement View visual.
