# GRANULAR SPRINT PART 1 — Tareas T001–T100
## Enfoque: Bug Fixes + Motor Espectral/Granular + Coherencia Reggaeton

> **Contexto obligatorio para GLM-5:**
> - MCP server corre en WSL. Todos los paths en `server.py` deben usar rutas Windows absolutas (ya configuradas). No cambies PROGRAM_DATA_DIR.
> - El Remote Script (`abletonmcp_init.py`) corre en el hilo de Live. NUNCA uses `time.sleep()` ahí.
> - Compila con `python -m py_compile` después de cada cambio. Reinicia Ableton después de cambiar `abletonmcp_init.py`.
> - Las herramientas MCP disponibles son: `get_session_info`, `get_tracks`, `get_track_info`, `create_arrangement_clip`, `add_notes_to_arrangement_clip`, `create_arrangement_audio_pattern`, `audit_project_coherence`, `set_device_parameter`, `delete_arrangement_clip`.
> - Proyecto activo: `C:\Users\ren\Desktop\song Project\song.als` — 95 BPM, clave Am, 16 tracks.

---

## BLOQUE A — BUG FIXES CRÍTICOS (T001–T015)

### T001 — Eliminar time.sleep del hilo Live
**Archivo:** `abletonmcp_init.py` ~línea 1450–1477
**Acción exacta:** Elimina el bloque `while total_wait < max_wait:` y sus `time.sleep(0.05)` de `_record_session_clip_to_arrangement`. Reemplaza con una sola búsqueda sin sleep:
```python
for tol in (0.05, 0.25, 1.0, 1.5):
    clip = self._locate_arrangement_clip(track, start_time, tol, length)
    if clip: break
if not clip:
    class ProxyClip:
        def __init__(self, l, n): self.length=l; self.name=n; self.start_time=start_time
        def set_notes(self, n): pass
    clip = ProxyClip(length, f"Proxy_{start_time}")
self._recent_arrangement_clips[(int(track_index), round(float(start_time),3))] = clip
return clip
```
**Valida:** `python -m py_compile abletonmcp_init.py` → sin errores.

### T002 — Eliminar time.sleep de duplicate_clip_to_arrangement
**Archivo:** `abletonmcp_init.py` ~línea 1581
**Acción:** Elimina `time.sleep(0.5)` dentro de `_create_arrangement_clip` en el bloque `duplicate_clip_to_arrangement`. Si no encuentra clip tras la búsqueda inmediata, devuelve ProxyClip igual que T001.

### T003 — Arreglar corrupción UTF-8 en headers de sample_selector.py
**Archivo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/sample_selector.py` líneas 5–17
**Problema:** Strings con `ÃƒÆ'Ã‚` (doble-encoding latin1→utf8). 
**Acción:** Reescribe los docstrings afectados con texto ASCII simple. No cambies lógica, solo los strings de documentación.

### T004 — Cleanup imports no usados en audio_analyzer.py
**Archivo:** `audio_analyzer.py` línea 317
**Acción:** Elimina `import struct` si existe y no se usa. Verifica con `grep -n "struct" audio_analyzer.py`.

### T005 — Cleanup imports no usados en sample_manager.py
**Archivo:** `sample_manager.py`
**Acción:** Elimina las líneas con `import os`, `import shutil`, `import time`, `from typing import Set` si no se usan en el cuerpo del archivo. Verifica antes con grep. Compila tras limpiar.

### T006 — Arreglar file_hash sin usar en sample_manager.py
**Archivo:** `sample_manager.py` ~línea 292
**Acción:** Cambia `file_hash = ...` a `_file_hash = ...` (prefijo underscore para suprimir warning F841) o elimina la asignación si no se usa en ninguna otra parte del método.

### T007 — WSL path normalization en _create_arrangement_audio_pattern
**Archivo:** `abletonmcp_init.py`, función `_create_arrangement_audio_pattern`
**Problema:** Cuando el MCP corre en WSL, los paths `/mnt/c/...` llegan al Remote Script que corre en Windows. El Remote Script necesita `C:\...`.
**Acción:** Al inicio de `_create_arrangement_audio_pattern`, añade:
```python
if str(file_path).startswith('/mnt/'):
    parts = file_path[5:].split('/', 1)
    file_path = parts[0].upper() + ":\\" + parts[1].replace('/', '\\')
```

### T008 — WSL path normalization en create_arrangement_clip (server.py)
**Archivo:** `server.py`, en el tool handler de `create_arrangement_audio_pattern`
**Acción:** Antes de enviar el comando al Remote Script, normaliza cualquier path `/mnt/c/...` a `C:\...`. Crea una función helper `_normalize_wsl_path(path: str) -> str` y úsala en todos los tool handlers que reciban `file_path` o `sample_path`.

### T009 — Enforce reinicio en KIMI_K2_ACTIVE_HANDOFF.md
**Archivo:** `KIMI_K2_ACTIVE_HANDOFF.md`
**Acción:** Añade sección al final: "## Cambios que requieren reinicio de Ableton" listando explícitamente qué archivos obligan a reiniciar (`abletonmcp_init.py`, `abletonmcp_runtime.py`, `AbletonMCP_AI/__init__.py`).

### T010 — Fix variables no usadas en song_generator.py
**Archivo:** `song_generator.py`
**Acción:** Las variables `materialized_track_roles` y `event_track_roles` se llenan pero nunca se leen. Añade un `self.log_message(f"[COHERENCE] materialized_track_roles={materialized_track_roles}")` donde corresponda, o elimínalas si son realmente inútiles.

### T011 — Arreglar tofix.md y actualizar fecha
**Archivo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/tofix.md`
**Acción:** Actualiza la fecha a 2026-04-05. Agrega las issues de T001–T010 a la sección "Ya corregido" una vez que estén resueltas.

### T012 — Verificar que generate_song_async no excede budget de 16 tracks
**Archivo:** `server.py`
**Acción:** Busca en `generate_song_async` o `_generate_track_impl` dónde se crea budget. Verifica que `GenerationBudget(max_tracks=16)` se instancia una sola vez por generación. Si hay múltiples instancias, consolida.

### T013 — Asegurar que MIDI hook tiene slot reservado antes de audio layers
**Archivo:** `server.py`
**Acción:** Después de `reset_budget(max_tracks=16)`, llama a `budget.reserve_slot('hook_midi', 'HARMONY_PIANO_MIDI', 'midi', 'mandatory_midi_hook')` antes de cualquier otra reserva. No elimines esta reserva hasta después de materializar el hook.

### T014 — Compilar todos los archivos modificados
**Acción post T001–T013:**
```powershell
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\abletonmcp_init.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\server.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\sample_selector.py"
python -m py_compile "C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\AbletonMCP_AI\MCP_Server\song_generator.py"
```
**Criterio de éxito:** Sin errores de compilación.

### T015 — Pedir reinicio de Ableton y verificar conexión
**Acción:** Informa al usuario: "Por favor reinicia Ableton Live ahora." Luego llama a `get_session_info`. Debe retornar BPM 95 y al menos 16 tracks. Si falla, revisa Log.txt.

---

## BLOQUE B — MOTOR ESPECTRAL GRANULAR (T016–T045)

### T016 — Crear módulo spectral_engine.py
**Archivo nuevo:** `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/spectral_engine.py`
**Propósito:** Motor de análisis espectral para comparar samples y asignarlos por similitud tímbrica, no solo por nombre o BPM. Es el núcleo de la "producción granular".
**Estructura mínima:**
```python
"""spectral_engine.py — Análisis espectral para selección por similitud tímbrica."""
import numpy as np
import logging
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass

logger = logging.getLogger("SpectralEngine")

@dataclass
class SpectralProfile:
    """Perfil espectral de un sample de audio."""
    path: str
    centroid_mean: float       # Hz — centro de masa espectral
    centroid_std: float        # Varianza del centroide
    rolloff_85: float          # Hz donde está el 85% de energía
    flux_mean: float           # Cambio espectral medio (percusividad)
    mfcc: List[float]          # 13 coeficientes MFCC normalizados
    rms: float                 # Energía RMS normalizada
    spectral_flatness: float   # 0=tonal, 1=ruido
    duration: float            # segundos
    genre_hints: List[str]     # géneros sugeridos por espectro

class SpectralEngine:
    def __init__(self):
        self._cache: Dict[str, SpectralProfile] = {}
        self._librosa = None
        self._np = np
        self._init_librosa()
    
    def _init_librosa(self):
        try:
            import librosa
            self._librosa = librosa
            logger.info("[SPECTRAL] librosa disponible")
        except ImportError:
            logger.warning("[SPECTRAL] librosa no disponible, usando análisis básico")
    
    def analyze(self, path: str) -> Optional[SpectralProfile]:
        if path in self._cache:
            return self._cache[path]
        if self._librosa:
            profile = self._analyze_librosa(path)
        else:
            profile = self._analyze_basic(path)
        if profile:
            self._cache[path] = profile
        return profile
    
    def similarity(self, a: SpectralProfile, b: SpectralProfile) -> float:
        """Retorna similitud 0.0–1.0 entre dos perfiles espectrales."""
        if not a or not b:
            return 0.0
        centroid_sim = 1.0 - min(abs(a.centroid_mean - b.centroid_mean) / max(a.centroid_mean + 1, 1), 1.0)
        rolloff_sim  = 1.0 - min(abs(a.rolloff_85 - b.rolloff_85) / max(a.rolloff_85 + 1, 1), 1.0)
        flux_sim     = 1.0 - min(abs(a.flux_mean - b.flux_mean) / max(a.flux_mean + 1, 1), 1.0)
        mfcc_sim = 0.0
        if a.mfcc and b.mfcc and len(a.mfcc) == len(b.mfcc):
            diff = sum((x-y)**2 for x,y in zip(a.mfcc, b.mfcc))
            mfcc_sim = 1.0 / (1.0 + diff**0.5)
        return 0.35*centroid_sim + 0.25*rolloff_sim + 0.15*flux_sim + 0.25*mfcc_sim

    def find_most_similar(self, reference_path: str, candidates: List[str], top_n: int = 5) -> List[Tuple[str, float]]:
        """Dado un sample de referencia, retorna los N candidatos más similares."""
        ref = self.analyze(reference_path)
        if not ref:
            return []
        scored = []
        for c in candidates:
            prof = self.analyze(c)
            if prof:
                score = self.similarity(ref, prof)
                scored.append((c, score))
        return sorted(scored, key=lambda x: x[1], reverse=True)[:top_n]

    def _analyze_librosa(self, path: str) -> Optional[SpectralProfile]:
        try:
            lib = self._librosa
            y, sr = lib.load(path, sr=None, mono=True, duration=30.0)
            centroid = lib.feature.spectral_centroid(y=y, sr=sr)[0]
            rolloff  = lib.feature.spectral_rolloff(y=y, sr=sr, roll_percent=0.85)[0]
            flux     = lib.feature.spectral_flux(y=y, sr=sr)[0] if hasattr(lib.feature, 'spectral_flux') else np.array([0.0])
            mfccs    = lib.feature.mfcc(y=y, sr=sr, n_mfcc=13)
            rms      = lib.feature.rms(y=y)[0]
            flatness = lib.feature.spectral_flatness(y=y)[0]
            duration = lib.get_duration(y=y, sr=sr)
            return SpectralProfile(
                path=path,
                centroid_mean=float(np.mean(centroid)),
                centroid_std=float(np.std(centroid)),
                rolloff_85=float(np.mean(rolloff)),
                flux_mean=float(np.mean(flux)),
                mfcc=[float(np.mean(mfccs[i])) for i in range(13)],
                rms=float(np.mean(rms)),
                spectral_flatness=float(np.mean(flatness)),
                duration=float(duration),
                genre_hints=self._infer_genre_hints(float(np.mean(centroid)), float(np.mean(rms)))
            )
        except Exception as e:
            logger.warning(f"[SPECTRAL] Error analizando {path}: {e}")
            return None

    def _analyze_basic(self, path: str) -> Optional[SpectralProfile]:
        import os
        name = os.path.basename(path).lower()
        centroid = 5000.0 if any(k in name for k in ['hat','shaker','top']) else (200.0 if 'bass' in name or 'sub' in name else 2000.0)
        return SpectralProfile(
            path=path, centroid_mean=centroid, centroid_std=100.0,
            rolloff_85=centroid*2, flux_mean=0.1, mfcc=[0.0]*13,
            rms=0.3, spectral_flatness=0.5 if 'noise' in name else 0.1,
            duration=2.0, genre_hints=self._infer_genre_hints(centroid, 0.3)
        )

    def _infer_genre_hints(self, centroid: float, rms: float) -> List[str]:
        hints = []
        if centroid < 500 and rms > 0.4: hints.append('reggaeton_bass')
        if 500 < centroid < 3000: hints.append('reggaeton_perc')
        if centroid > 6000: hints.append('hi_freq_perc')
        return hints or ['unknown']

_engine_instance: Optional[SpectralEngine] = None

def get_spectral_engine() -> SpectralEngine:
    global _engine_instance
    if _engine_instance is None:
        _engine_instance = SpectralEngine()
    return _engine_instance
```
**Valida:** `python -m py_compile spectral_engine.py`

### T017 — Integrar SpectralEngine en sample_selector.py
**Archivo:** `sample_selector.py`
**Acción:** En el método de scoring principal (donde se calcula el score final de un candidato), añade un bonus espectral si `SpectralEngine` está disponible:
```python
try:
    from .spectral_engine import get_spectral_engine
    eng = get_spectral_engine()
    if reference_path and eng:
        ref_prof = eng.analyze(reference_path)
        cand_prof = eng.analyze(candidate_path)
        if ref_prof and cand_prof:
            spectral_bonus = eng.similarity(ref_prof, cand_prof) * 0.25
            score = score * 0.75 + spectral_bonus
except Exception:
    pass
```

### T018 — Añadir MCP tool: analyze_sample_spectrum
**Archivo:** `server.py`
**Acción:** Añade un tool:
```python
@mcp.tool()
async def analyze_sample_spectrum(file_path: str) -> str:
    """Analiza el espectro de un sample y retorna su perfil tímbrico."""
    from spectral_engine import get_spectral_engine
    eng = get_spectral_engine()
    profile = eng.analyze(file_path)
    if not profile:
        return "[ERROR] No se pudo analizar el sample"
    return json.dumps({
        "centroid_hz": round(profile.centroid_mean, 1),
        "rolloff_85_hz": round(profile.rolloff_85, 1),
        "spectral_flatness": round(profile.spectral_flatness, 3),
        "duration_s": round(profile.duration, 2),
        "genre_hints": profile.genre_hints
    }, indent=2)
```

### T019 — Añadir MCP tool: find_similar_samples
**Archivo:** `server.py`
**Acción:** Añade:
```python
@mcp.tool()
async def find_similar_samples(reference_path: str, search_folder: str, top_n: int = 5) -> str:
    """Encuentra los N samples más similares espectralmente al de referencia."""
    import os
    from spectral_engine import get_spectral_engine
    eng = get_spectral_engine()
    candidates = [os.path.join(search_folder, f) for f in os.listdir(search_folder)
                  if f.lower().endswith(('.wav', '.aif', '.aiff', '.mp3'))]
    results = eng.find_most_similar(reference_path, candidates, top_n=top_n)
    return json.dumps([{"path": p, "similarity": round(s, 3)} for p, s in results], indent=2)
```

### T020 — Crear índice espectral de la librería reggaeton
**Archivo:** crear `AbletonMCP_AI/AbletonMCP_AI/MCP_Server/build_spectral_index.py`
**Propósito:** Script offline que pre-analiza toda la librería y guarda un JSON con perfiles para que en runtime no se recalcule.
```python
#!/usr/bin/env python3
"""Construye índice espectral de la librería de samples."""
import json, os, sys
sys.path.insert(0, os.path.dirname(__file__))
from spectral_engine import get_spectral_engine

LIBRARY = r"C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\libreria\reggaeton"
INDEX_FILE = os.path.join(os.path.dirname(__file__), "spectral_index.json")

def build():
    eng = get_spectral_engine()
    index = {}
    for root, dirs, files in os.walk(LIBRARY):
        for f in files:
            if f.lower().endswith(('.wav','.aif','.aiff')):
                path = os.path.join(root, f)
                prof = eng.analyze(path)
                if prof:
                    index[path] = {
                        "centroid": prof.centroid_mean,
                        "rolloff": prof.rolloff_85,
                        "flux": prof.flux_mean,
                        "mfcc": prof.mfcc,
                        "rms": prof.rms,
                        "flatness": prof.spectral_flatness,
                        "duration": prof.duration,
                        "genre_hints": prof.genre_hints
                    }
                    print(f"OK: {f}")
    with open(INDEX_FILE, 'w') as fh:
        json.dump(index, fh, indent=2)
    print(f"Índice guardado: {len(index)} samples en {INDEX_FILE}")

if __name__ == "__main__":
    build()
```
**Ejecuta:** `python build_spectral_index.py` (puede tardar 2–5 minutos si librosa está disponible).

### T021 — Cargar índice espectral en SpectralEngine.__init__
**Archivo:** `spectral_engine.py`
**Acción:** En `__init__`, si existe `spectral_index.json`, carga los perfiles al cache para evitar recalcular:
```python
import json, os
INDEX_PATH = os.path.join(os.path.dirname(__file__), "spectral_index.json")
if os.path.exists(INDEX_PATH):
    with open(INDEX_PATH) as fh:
        data = json.load(fh)
    for path, d in data.items():
        self._cache[path] = SpectralProfile(path=path, **d)
    logger.info(f"[SPECTRAL] Índice cargado: {len(self._cache)} samples")
```

### T022 — Añadir perfil espectral de referencia al genre profile reggaeton
**Archivo:** `sample_selector.py`, en `GENRE_PROFILES['reggaeton']`
**Acción:** Añade el campo `spectral_targets` al `GenreProfile` de reggaeton (requiere modificar la clase `GenreProfile` para aceptar el parámetro opcional):
```python
'reggaeton': GenreProfile(
    name='Reggaeton',
    bpm_range=(88, 98),
    common_keys=['Dm', 'Am', 'Fm', 'Gm', 'Cm'],
    drum_pattern='dembow',
    bass_style='subby',
    characteristics=['latin', 'syncopated', 'urban', 'percussive'],
    # T022: spectral targets for reggaeton
    # centroid_hz: kick~200, bass~400, perc~3000, hat~8000
    spectral_targets={
        'kick': {'centroid_range': (100, 400), 'flatness_max': 0.15},
        'bass': {'centroid_range': (200, 800), 'flatness_max': 0.2},
        'perc': {'centroid_range': (1500, 5000), 'flatness_max': 0.4},
        'hat':  {'centroid_range': (5000, 16000), 'flatness_max': 0.7},
    }
)
```

### T023–T030 — Integración espectral profunda en SampleSelector
**T023:** En `SampleSelector.select_for_role(role, genre, ...)`, después del score base, llama a `SpectralEngine` para penalizar samples espectralmente incompatibles con el género.
**T024:** Añade `spectral_coherence_score` al `SampleDecision.to_log_str()` para trackeabilidad.
**T025:** Si el género es `reggaeton` y el rol es `bass`, rechaza muestras con `centroid_mean > 1500 Hz` (serían demasiado brillantes para un bajo dembow).
**T026:** Si el género es `reggaeton` y el rol es `kick`, rechaza muestras con `duration > 1.5s` (los kicks de reggaeton son agresivos y cortos).
**T027:** Para `synth_loop` en reggaeton: prioriza samples con `spectral_flatness < 0.3` (tonal, no ruidoso).
**T028:** Para `top_loop` en reggaeton: acepta `spectral_flatness` hasta 0.6 (las percusiones lat. tienen algo de ruido).
**T029:** Añade log `[SPECTRAL_GATE]` cuando un sample es rechazado por criterios espectrales.
**T030:** Valida con `python -m pytest tests/test_sample_selector.py -v` que los tests existentes siguen pasando.

### T031–T040 — Análisis espectral de referencia (reference_listener.py)
**T031:** En `reference_listener.py`, en `_compute_segment_features`, añade llamada a `SpectralEngine.analyze(reference_path)` y almacena el perfil en `self._reference_spectral_profile`.
**T032:** Expón `reference_spectral_profile` en el resultado de `analyze_reference()` como campo `spectral_profile`.
**T033:** En `server.py`, cuando se llama a `analyze_reference`, guarda el perfil espectral en una variable global `_reference_spectral_profile`.
**T034:** Al seleccionar samples para una generación con referencia, pasa `_reference_spectral_profile` al `SampleSelector` para que el score de similitud espectral use la referencia real, no targets genéricos.
**T035:** En `reference_listener.py`, calcula el `centroid_mean` del stem percusivo de la referencia y almacénalo como `reference_perc_centroid`.
**T036:** En `reference_listener.py`, calcula el `centroid_mean` del stem de bajo de la referencia y almacénalo como `reference_bass_centroid`.
**T037:** Expón `reference_perc_centroid` y `reference_bass_centroid` en el resultado de `analyze_reference`.
**T038:** Añade MCP tool `get_reference_spectral_targets() -> str` que retorna los targets espectrales detectados de la referencia activa.
**T039:** En `coherence_analyzer.py`, añade un nuevo metric `SpectralCoherenceMetric` que mide cuántos samples del manifest están dentro del rango espectral de la referencia.
**T040:** Añade `spectral_coherence` al `CoherenceReport.to_dict()`.

### T041–T045 — Índice vectorial ligero para búsqueda por similitud
**T041:** En `spectral_engine.py`, añade método `build_similarity_matrix(paths: List[str]) -> np.ndarray` que calcula la matriz de similitud NxN entre todos los samples de la librería.
**T042:** Añade método `cluster_by_role(paths: List[str], n_clusters: int = 5) -> Dict[int, List[str]]` que agrupa samples en N familias tímbricas sin necesidad de scikit-learn (usa K-means manual con numpy).
**T043:** Ejecuta `build_similarity_matrix` sobre la carpeta `libreria/reggaeton/perc loop/` y guarda el resultado como `perc_loop_clusters.json`.
**T044:** En `sample_selector.py`, cuando el rol es `perc_loop` o `top_loop`, consulta `perc_loop_clusters.json` y fuerza que samples de la misma sesión vengan del mismo cluster tímbrico (coherencia de color percusivo).
**T045:** Añade test unitario `test_spectral_engine.py` con: test de creación sin librosa (análisis básico), test de similitud entre dos perfiles iguales (debe ser 1.0), test de similitud entre perfiles opuestos (debe ser < 0.3).

---

## BLOQUE C — REGGAETON ESPECÍFICO (T046–T065)

### T046 — Actualizar GENRE_PROFILES['reggaeton'] en sample_selector.py
**Acción:** El perfil actual tiene `bpm_range=(88, 98)`. El proyecto usa 95 BPM. Cambia la descripción del `drum_pattern` a `'dembow_95bpm'` y añade `'moombahton'` como alias.

### T047 — Añadir perfil de género 'perreo' distinto de 'reggaeton'
**Archivo:** `sample_selector.py`
**Acción:** Agrega:
```python
'perreo': GenreProfile(
    name='Perreo',
    bpm_range=(90, 96),
    common_keys=['Am', 'Dm', 'Gm'],
    drum_pattern='dembow_hard',
    bass_style='reese_sub',
    characteristics=['dark', 'hard', 'urban', 'bass_heavy']
)
```

### T048 — Añadir a song_generator.py la progresión Am reggaeton canónica
**Archivo:** `song_generator.py`
**Acción:** Busca donde se definen progresiones de acordes por género. Añade para reggaeton:
```python
'reggaeton': {
    'drop':     ['Am', 'F', 'G', 'Em'],  # clásico perreo
    'break':    ['Am', 'G', 'F', 'E'],   # tensión
    'intro':    ['Am', 'F', 'C', 'G'],   # suave
    'build':    ['Dm', 'Am', 'G', 'Am'], # sube
}
```

### T049 — Implementar dembow pattern correcto en drum grid
**Archivo:** `song_generator.py`
**Acción:** El patrón dembow estándar en una grilla de 16 corcheas (una barra de 4/4) es:
```
Kick:  X . . . . . . X . X . . X . . .   (1, 8, 10, 13)
Snare: . . . . X . . . . . . . . . . .   (5)
Hat:   X . X . X . X . X . X . X . X .   (cada corchea par)
```
Verifica que el generador de patrones produce esta distribución para reggaeton/perreo. Si no, corrígela.

### T050 — Bass line dembow bouncy con slides
**Archivo:** `song_generator.py`
**Acción:** La línea de bajo dembow tiene "tumbao": nota en el 1, silencio, nota sincopada corta en el 2-y, nota en el 3. Verifica que `create_bassline(style='dembow')` ó `style='bouncy'` produce notas en posiciones `[0, 0.5, 1.5, 2, 2.5, 3]` (en beats dentro de la barra). Si no, corrígelo.

### T051 — Añadir variante de bajo 'reese_reggaeton'
**Archivo:** `song_generator.py`
**Acción:** El bajo Reese en reggaeton es un bajo distorsionado y subterráneo. Añade el estilo a la función de bajo con parámetros de nota más bajos (octava 1-2) y duración más larga (sostenida).

### T052 — Asegurar que section_aware selection prioriza dembow para drop
**Archivo:** `sample_selector.py`, `SECTION_ROLE_PROFILES['drop']`
**Acción:** Verifica que en drop, `perc_loop` está en `primary`. Añade `perc_alt` como rol secundario si no existe. Para reggaeton, el drop debe tener kick + perc loop dembow siempre activos.

### T053 — Implementar regla: no hay intro sin kick en reggaeton
**Archivo:** `song_generator.py` o `server.py`
**Acción:** Para género reggaeton, en el intro, el kick debe entrar desde el beat 0 (no desde el beat 16 como en techno). Añade guardia en la lógica de intro que force `kick_present=True` para reggaeton desde el inicio.

### T054 — Corrección de pitch: notas MIDI en clave Am
**Archivo:** `song_generator.py`, método de generación harmónica
**Acción:** Verifica que todas las notas generadas para reggaeton en Am corresponden a la escala Am natural: A(69), B(71), C(72), D(74), E(76), F(77), G(79). Si hay notas fuera de escala, añade un filtro `_quantize_to_scale(note, scale_notes)`.

### T055 — Añadir MCP tool: populate_harmony_track
**Archivo:** `server.py`
**Acción:**
```python
@mcp.tool()
async def populate_harmony_track(track_index: int = 15, key: str = "Am", bpm: float = 95.0) -> str:
    """Rellena el track MIDI harmónico con progresiones Am para reggaeton."""
    PROGRESSION = [
        (0,   32, [('A3',1.0),('C4',0.5),('E4',0.5)]),   # Am
        (32,  32, [('F3',1.0),('A3',0.5),('C4',0.5)]),   # F
        (64,  32, [('G3',1.0),('B3',0.5),('D4',0.5)]),   # G
        (96,  32, [('E3',1.0),('G3',0.5),('B3',0.5)]),   # Em
        (128, 32, [('A3',1.0),('C4',0.5),('E4',0.5)]),   # Am repeat
        (160, 32, [('F3',1.0),('A3',0.5),('C4',0.5)]),   # F repeat
        (192, 32, [('G3',1.0),('D4',1.0),('B3',0.5)]),   # G build
        (224, 32, [('A3',2.0),('E4',2.0)]),               # Am outro
    ]
    results = []
    for start, length, notes in PROGRESSION:
        r = ableton.send_command("create_arrangement_clip", {"track_index": track_index, "start_time": start, "length": length})
        if not _is_error_response(r):
            midi_notes = [{"pitch": _note_name_to_midi(n), "start_time": i*4.0, "duration": d*4.0, "velocity": 80} for i,(n,d) in enumerate(notes)]
            ableton.send_command("add_notes_to_arrangement_clip", {"track_index": track_index, "start_time": start, "notes": midi_notes})
            results.append(f"OK beat {start}")
        else:
            results.append(f"FAIL beat {start}: {r.get('message','')}")
    return "\n".join(results)
```

### T056–T065 — Más mejoras reggaeton específicas
**T056:** Añade `_note_name_to_midi(name: str) -> int` a server.py como helper que convierte "A3"→57, "C4"→60, etc.
**T057:** En `coherence_analyzer.py`, para reggaeton, baja el target de `max_harmonic_gap_beats` de 8 a 16 (es aceptable tener breaks de hasta 2 compases sin harmónicos en reggaeton).
**T058:** Añade `reggaeton_perc_density_score` al CoherenceReport: mide si hay perc loop en ≥70% del arrangement.
**T059:** En `sample_selector.py`, para reggaeton, la familia dominante debe ser del pack `Midilatino` o `SentimientoLatino` si están disponibles, no un pack genérico.
**T060:** Añade un guardia en `server.py`: si el género es `reggaeton` y se detecta que el tempo real difiere de 95 BPM ±3, emite un warning `[REGGAETON_WARNING] BPM fuera de rango estándar`.
**T061:** Para achoques de perc en reggaeton (Track 11/12), el clip óptimo es de 16 beats (4 compases), no 8. Actualiza la lógica de `create_arrangement_audio_pattern` para que reggaeton use `default_clip_length=16`.
**T062:** Añade `perreo_style_profile` en `reference_listener.py` que detecta si un audio de referencia tiene patrón dembow (sub 100Hz regular cada ~0.63s a 95 BPM) y setea `detected_style='perreo'`.
**T063:** Si `detected_style='perreo'`, en la selección de samples prioriza packs con keywords 'latin', 'urbano', 'perreo', 'reggaeton', 'dembow' en su path.
**T064:** Añade test: `test_reggaeton_coherence.py` que verifica que una generación reggaeton produce drum_coverage_ratio > 0.6.
**T065:** Actualiza `KIMI_K2_ACTIVE_HANDOFF.md` con el estado de los módulos nuevos (spectral_engine.py, populate_harmony_track tool).

---

## BLOQUE D — COHERENCIA Y DIVERSIDAD (T066–T085)

### T066 — Forzar mismo-pack en reggaeton
**Archivo:** `sample_selector.py`
**Acción:** Añade método `force_pack_lock(pack_name: str)` que, cuando se llama, penaliza (score * 0.1) cualquier sample que no pertenezca al pack especificado. Llama a este método después de detectar el pack dominante en la primera selección.

### T067 — Anti-mirror: detectar secciones especulares
**Archivo:** `coherence_analyzer.py`
**Acción:** Añade `MirrorSectionMetric` que cuenta cuántos pares de secciones son idénticos (mismos samples en los mismos beats relativos). Target: `mirror_pairs < 4`. Escribe el métrodo `_count_mirror_pairs(manifest)`.

### T068 — Reducir repetición: sample cooldown entre sections
**Archivo:** `sample_selector.py`
**Acción:** Después de usar un sample en la sección `drop`, agrega el path a una cola `_section_cooldown_queue` con TTL de 2 secciones. En las siguientes 2 secciones, ese sample tiene penalización del 50%.

### T069 — Diversity check antes de confirmar sample
**Archivo:** `sample_selector.py`
**Acción:** Antes de confirmar una selección, verifica que el mismo sample no aparece más de `COOLDOWN_WINDOW=3` veces en el arrangement actual. Si lo hace, fuerza selección del siguiente candidato.

### T070 — Añadir campo 'section_kind' a todos los logs de selección
**Archivo:** `sample_selector.py`
**Acción:** Cada entry de log de `SampleDecision.to_log_str()` debe incluir el `section_kind` actual para poder trazar qué sample se usó en qué sección.

### T071 — Fix pack coherence: _extract_pack no considere carpetas genéricas
**Archivo:** `reference_listener.py` o `sample_selector.py`, método `_extract_pack`
**Acción:** Las carpetas `20 One Shots`, `loop`, `perc loop` son carpetas de categoría, no de pack. El pack debe extraerse del abuelo de la carpeta. Verifica la lógica y corrige si es necesario.

### T072–T080 — Métricas de coherencia adicionales
**T072:** Añade `LayerCountBySection` a CoherenceReport: para cada sección, cuenta cuántos layers hay activos. Target: drop tiene más layers que break.
**T073:** Añade `BassPresenceRatio`: ratio de tiempo en que el bass está activo vs total. Target > 0.80 para reggaeton.
**T074:** Añade `KickPresenceRatio`: target > 0.65 para reggaeton.
**T075:** Añade `HatPresenceRatio`: target > 0.65 para reggaeton.
**T076:** Añade `EnergyArcScore`: mide si la energía (capas activas) sube del intro al drop y baja en el break. Target: `arc_score > 0.6`.
**T077:** Expón todas las nuevas métricas en `audit_project_coherence()` MCP tool.
**T078:** Actualiza `CoherenceReport.to_dict()` para incluir todas las nuevas métricas de T072–T077.
**T079:** Escribe test unitario para cada nueva métrica de T072–T077.
**T080:** Actualiza `roadmap.md` marcando los ítems completados de FASE 4 (Spectral Fingerprinting). 

### T081–T085 — Diversity Memory improvements
**T081:** En `diversity_memory.py`, añade persistencia de `spectral_family` además de `sample_family`. Cuando se registra un sample usado, guarda también su `centroid_bucket` (low/mid/high freq) para evitar repetición espectral inter-sesión.
**T082:** Añade método `get_spectral_penalty(centroid_bucket: str, role: str) -> float` que devuelve penalización si ese bucket ya fue usado recientemente para ese rol.
**T083:** En `sample_selector.py`, después de calcular score base, aplica `get_spectral_penalty` si diversity_memory está disponible.
**T084:** Añade `diversity_memory.export_stats() -> Dict` que retorna estadísticas de uso: top 5 familias usadas, top 5 cenroid_buckets usados.
**T085:** Expón las stats en un MCP tool `get_diversity_stats() -> str`.

---

## BLOQUE E — ARRANGEMENT INTELIGENTE (T086–T100)

### T086 — Crear módulo arrangement_intelligence.py
**Archivo nuevo:** `arrangement_intelligence.py`
**Propósito:** Lógica de arrangement de nivel DJ para reggaeton.
```python
"""arrangement_intelligence.py — Lógica de arrangement para DJ profesional."""

REGGAETON_STRUCTURE_95BPM = {
    'intro':  {'start': 0,   'length': 32,  'energy': 0.3, 'layers': ['kick','hat','bass']},
    'build_a':{'start': 32,  'length': 32,  'energy': 0.6, 'layers': ['kick','hat','clap','bass','perc_main']},
    'drop_a': {'start': 64,  'length': 64,  'energy': 1.0, 'layers': ['kick','hat','clap','bass','perc_main','perc_alt','synth']},
    'break':  {'start': 128, 'length': 32,  'energy': 0.2, 'layers': ['bass','synth','atmos']},
    'build_b':{'start': 160, 'length': 32,  'energy': 0.7, 'layers': ['kick','hat','clap','bass','perc_main','synth']},
    'drop_b': {'start': 192, 'length': 64,  'energy': 1.0, 'layers': ['kick','hat','clap','bass','perc_main','perc_alt','synth','top_loop']},
    'outro':  {'start': 256, 'length': 32,  'energy': 0.2, 'layers': ['kick','hat','bass']},
}
```

### T087 — Añadir MCP tool: apply_reggaeton_structure
**Archivo:** `server.py`
**Acción:** Tool que aplica la estructura de T086 al proyecto activo: llama a MCP para verificar qué tracks existen, mapea roles a índices, y configura los clips para seguir la estructura.

### T088 — Implementar mute throws (silencio antes del drop)
**Archivo:** `server.py` o `arrangement_intelligence.py`
**Acción:** Para los beats 61–64 (3 beats antes del drop_a) y beats 189–192 (antes del drop_b), elimina o silencia los clips de kick, hat y clap. Esto crea el "pull-back" que hace que el drop golpee más fuerte.

### T089 — Implementar energy curve checker
**Archivo:** `arrangement_intelligence.py`
**Acción:** Método `check_energy_curve(track_clips: Dict[str, List]) -> float` que dado el mapa de clips por track, calcula la curva de energía (capas activas por cada 16 beats) y retorna un score de 0–1 indicando qué tan bien sigue la curva intro→build→drop→break→drop→outro.

### T090 — Añadir tool: audit_arrangement_structure
**Archivo:** `server.py`
**Acción:** Tool que llama a `get_tracks`, analiza los clips por sección y retorna un reporte de energía por sección, gaps detectados, y si la estructura está incompleta.

### T091–T100 — Filling y patching de arrangement
**T091:** Para el track de harmónicos (índice 15), si tiene 0 arrangement clips, automáticamente ejecuta `populate_harmony_track` de T055.
**T092:** Para el track de top_loop (índice 12), si tiene gaps > 32 beats, rellena con el sample más frecuentemente usado en ese track.
**T093:** Para el track perc_alt (índice 11), si tiene gaps > 32 beats, rellena con alternancia de perc 1 y perc 2.
**T094:** Añade MCP tool `fill_arrangement_gaps(max_gap_beats: int = 32)` que ejecuta T091–T093 automáticamente.
**T095:** En `coherence_analyzer.py`, detecta si hay más de 5 secciones especulares (mirror) y reporta en `redundant_layers`.
**T096:** Añade recomendación automática en el CoherenceReport: si `drum_coverage < 0.55`, sugiere ejecutar `fill_arrangement_gaps`.
**T097:** Añade recomendación: si `harmonic_coverage < 0.60`, sugiere ejecutar `populate_harmony_track`.
**T098:** Crea `docs/SPECTRAL_ENGINE_README.md` documentando cómo usar el motor espectral, cómo regenerar el índice, y cómo interpretar los resultados.
**T099:** Actualiza `AGENTS.md` con los nuevos módulos: `spectral_engine.py`, `arrangement_intelligence.py`, `build_spectral_index.py`.
**T100:** Ejecuta el smoke test completo y documenta los resultados en `docs/SPRINT_GRANULAR_PART1_VALIDATION.md`.
