"""
pattern_library.py - Biblioteca de patrones musicales profesionales para reggaeton

Contiene patrones de dembow, bajos, progresiones de acordes, generadores de melodÃ­as
y utilidades para humanizaciÃ³n.

Timing en beats (float), reggaeton tÃ­picamente 4/4 @ 90-100 BPM
"""

import random
from typing import List, Tuple, Optional, Dict, Any
from dataclasses import dataclass
from enum import Enum


@dataclass
class NoteEvent:
    """Representa un evento de nota MIDI"""
    pitch: int
    start_time: float  # En beats
    duration: float      # En beats
    velocity: int        # 0-127
    
    def copy(self) -> 'NoteEvent':
        return NoteEvent(self.pitch, self.start_time, self.duration, self.velocity)


class ScaleType(Enum):
    MINOR = "minor"
    MAJOR = "major"
    PENTATONIC_MINOR = "pentatonic_minor"
    BLUES = "blues"


class DembowPatterns:
    """
    Patrones de dembow profesionales para reggaeton.
    El dembow es el ritmo caracterÃ­stico del reggaeton.
    """
    
    # Notas MIDI estÃ¡ndar para drums
    KICK_NOTE = 36      # C1
    SNARE_NOTE = 38     # D1
    HIHAT_CLOSED = 42   # F#1
    HIHAT_OPEN = 46     # A#1
    CLAP_NOTE = 39      # D#1
    RIMSHOT_NOTE = 37   # C#1
    
    # Tiempos de dembow en beats (cada beat = 1 cuarto nota)
    # PatrÃ³n clÃ¡sico: kick en 1, snare en 2.25 y 4, etc.
    
    @staticmethod
    def get_kick_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]:
        """
        Genera patrÃ³n de kick/bombo.
        
        Variaciones:
        - standard: PatrÃ³n dembow clÃ¡sico
        - double: Doble tiempo en ciertos beats
        - triple: PatrÃ³n tresillo
        - minimal: Menos kicks, mÃ¡s espacio
        """
        notes = []
        beat_duration = 0.25  # 1/16 nota = 0.25 beats
        
        if variation == "standard":
            # Dembow clÃ¡sico: kick en 1, 3, 4.25, 4.75 de cada compÃ¡s
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Kick en tiempo 1 (beat 0 del compÃ¡s)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 0.0,
                    0.25,
                    120
                ))
                # Kick en tiempo 3 (beat 2 del compÃ¡s)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 2.0,
                    0.25,
                    110
                ))
                # Kick ghost en 4.25 (anticipaciÃ³n)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.25,
                    0.125,
                    80
                ))
                # Kick en 4.75 (cierre)
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.75,
                    0.125,
                    90
                ))
                
        elif variation == "double":
            # MÃ¡s kicks, doble tiempo en ciertos momentos
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Kick fuerte en 1
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 127))
                # Kick en off-beat
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.75, 0.125, 100))
                # Kick en 2.5
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 1.5, 0.25, 115))
                # Kick en 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 120))
                # Kick en off-beat 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.75, 0.125, 95))
                # Dos kicks rÃ¡pidos al final
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.25, 0.125, 90))
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.125, 100))
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.75, 0.125, 110))
                
        elif variation == "triple":
            # PatrÃ³n tresillo mÃ¡s complejo
            tresillo_interval = 4.0 / 3.0  # Tresillo = 1.333 beats
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i in range(3):
                    notes.append(NoteEvent(
                        DembowPatterns.KICK_NOTE,
                        bar_offset + (i * tresillo_interval),
                        0.3,
                        120 if i == 0 else 100
                    ))
                # Kick adicional en el Ãºltimo 16vo
                notes.append(NoteEvent(
                    DembowPatterns.KICK_NOTE,
                    bar_offset + 3.75,
                    0.125,
                    90
                ))
                
        elif variation == "minimal":
            # Estilo minimal, menos es mÃ¡s
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Solo kick en 1 y 3
                notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 125))
                if bar % 2 == 0:  # Cada dos compases
                    notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 110))
                    # Sub-bajo sutil en 4
                    notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.25, 85))
                    
        else:
            raise ValueError(f"VariaciÃ³n de kick no vÃ¡lida: {variation}")
            
        return notes
    
    @staticmethod
    def get_snare_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]:
        """
        Genera patrÃ³n de snare/caja.
        
        El dembow clÃ¡sico tiene snare en 2.25 (beat 2 + 1/4) y 4.
        """
        notes = []
        
        if variation == "standard":
            # Snare clÃ¡sico dembow: tiempo 2.25 y 4
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Snare principal en 2.25 (el caracterÃ­stico)
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 1.25,  # Beat 2 + 1/4
                    0.15,
                    115
                ))
                # Snare en 4
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 3.0,
                    0.2,
                    120
                ))
                # Ghost note sutil en 2.75
                if bar % 2 == 1:  # Cada dos compases
                    notes.append(NoteEvent(
                        DembowPatterns.RIMSHOT_NOTE,
                        bar_offset + 1.75,
                        0.1,
                        70
                    ))
                    
        elif variation == "double":
            # MÃ¡s snares, estilo mÃ¡s agresivo
            for bar in range(bars):
                bar_offset = bar * 4.0
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.0, 0.15, 110))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.25, 0.15, 120))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.0, 0.2, 125))
                # Roll en el Ãºltimo beat
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.5, 0.1, 100))
                notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.75, 0.1, 90))
                
        elif variation == "triple":
            # PatrÃ³n tresillo para snare
            tresillo_offsets = [1.0, 2.333, 3.666]
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i, offset in enumerate(tresillo_offsets):
                    notes.append(NoteEvent(
                        DembowPatterns.SNARE_NOTE,
                        bar_offset + offset,
                        0.2,
                        115
                    ))
                    
        elif variation == "minimal":
            # Snare minimalista
            for bar in range(bars):
                bar_offset = bar * 4.0
                notes.append(NoteEvent(
                    DembowPatterns.SNARE_NOTE,
                    bar_offset + 1.25,
                    0.15,
                    110
                ))
                # Solo en compases pares el segundo snare
                if bar % 2 == 0:
                    notes.append(NoteEvent(
                        DembowPatterns.SNARE_NOTE,
                        bar_offset + 3.0,
                        0.2,
                        105
                    ))
                    
        return notes
    
    @staticmethod
    def get_hihat_pattern(bars: int = 16, style: str = "8th", swing: float = 0.6) -> List[NoteEvent]:
        """
        Genera patrÃ³n de hi-hats.
        
        Estilos: "8th", "16th", "32nd", "open", "pedal"
        Swing: 0.0-1.0, donde 0.5 es recto, >0.5 es swingado
        """
        notes = []
        
        # Factor de swing: cuÃ¡nto se retrasa el off-beat
        swing_amount = (swing - 0.5) * 0.5  # Rango -0.25 a +0.25
        
        if style == "8th":
            # Corcheas: en cada 1/2 beat
            for bar in range(bars):
                bar_offset = bar * 4.0
                for eighth in range(8):
                    beat_pos = bar_offset + (eighth * 0.5)
                    # Aplicar swing a los off-beats (impares)
                    if eighth % 2 == 1:
                        beat_pos += swing_amount
                    
                    # DinÃ¡mica: acentos en 2 y 4
                    velocity = 100
                    if eighth in [2, 6]:  # Tiempos 1.0 y 3.0 (beats 2 y 4)
                        velocity = 115
                    elif eighth in [0, 4]:  # Downbeats
                        velocity = 110
                    else:
                        velocity = 90
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.1,
                        velocity
                    ))
                    
        elif style == "16th":
            # Semicorcheas: mÃ¡s denso
            for bar in range(bars):
                bar_offset = bar * 4.0
                for sixteenth in range(16):
                    beat_pos = bar_offset + (sixteenth * 0.25)
                    # Swing en off-beats
                    if sixteenth % 2 == 1:
                        beat_pos += swing_amount * 0.5
                    
                    # Pattern de velocidades tipo "trap"
                    if sixteenth % 4 == 0:  # Cuartos
                        velocity = 110
                    elif sixteenth % 2 == 0:  # Octavas
                        velocity = 95
                    else:  # 16avos
                        velocity = 85
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.08,
                        velocity
                    ))
                    
        elif style == "32nd":
            # Fusas: muy denso, estilo moderno
            for bar in range(bars):
                bar_offset = bar * 4.0
                for i in range(32):
                    beat_pos = bar_offset + (i * 0.125)
                    # Roll de 32avos en el Ãºltimo beat
                    if i >= 28:
                        velocity = 100 + (i - 28) * 5  # Crescendo
                    else:
                        velocity = 80 if i % 2 == 1 else 70
                        
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.05,
                        velocity
                    ))
                    
        elif style == "open":
            # Hi-hat abierto en ciertos tiempos
            open_times = [1.5, 3.5]  # Off-beats de 2 y 4
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Cerrados en corcheas
                for eighth in range(8):
                    beat_pos = bar_offset + (eighth * 0.5)
                    if eighth % 2 == 1:
                        beat_pos += swing_amount
                    
                    # Verificar si es tiempo de abierto
                    time_in_bar = eighth * 0.5
                    if any(abs(time_in_bar - ot) < 0.01 for ot in open_times):
                        # Hi-hat abierto
                        notes.append(NoteEvent(
                            DembowPatterns.HIHAT_OPEN,
                            beat_pos,
                            0.3,  # MÃ¡s largo
                            110
                        ))
                    else:
                        notes.append(NoteEvent(
                            DembowPatterns.HIHAT_CLOSED,
                            beat_pos,
                            0.1,
                            100
                        ))
                        
        elif style == "pedal":
            # Estilo pedal - mÃ¡s sutil
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Solo en corcheas pares, suave
                for eighth in [0, 2, 4, 6]:
                    beat_pos = bar_offset + (eighth * 0.5)
                    notes.append(NoteEvent(
                        DembowPatterns.HIHAT_CLOSED,
                        beat_pos,
                        0.15,
                        75
                    ))
                    
        return notes


class BassPatterns:
    """
    Patrones de bajo sub para reggaeton profesional.
    Sprint 7: 8 estilos de bajo avanzados con mapeo a scenes.
    """
    
    # Notas MIDI para bajo (C1 = 36, generalmente)
    
    # Sprint 7 - Fases 71-75: 8 estilos de bass
    BASS_STYLES = {
        "sub": {"pattern": [0], "duration": 3.5, "octave": -1, "description": "Sub-bajos largos y profundos"},
        "sustained": {"pattern": [0, 2], "duration": 1.8, "description": "Notas sostenidas con release"},
        "pluck": {"pattern": [0, 0.5, 1.5, 2.5], "duration": 0.3, "description": "Notas cortas y percusivas"},
        "slap": {"pattern": [0, 0.75, 2, 2.75], "duration": 0.4, "description": "Estilo slap con ataque fuerte"},
        "slide": {"pattern": [0, 1, 2, 3], "duration": 0.8, "slide": True, "description": "Con slides entre notas"},
        "octaves": {"pattern": [0, 0.5, 1, 1.5], "duration": 0.4, "octaves": True, "description": "Alternando octavas"},
        "harmonics": {"pattern": [0, 2], "duration": 1.5, "harmonic": True, "description": "ArmÃ³nicos artificiales"},
        "synth": {"pattern": [0, 0.75, 1.5, 2.25], "duration": 0.6, "description": "Estilo sintetizador de onda"}
    }
    
    # Sprint 7 - Mapeo de scenes a estilos de bass
    SCENE_BASS_MAP = {
        "intro": "sub",
        "verse": "pluck", 
        "chorus": "octaves",
        "bridge": "sustained",
        "build": "synth",
        "drop": "slap",
        "outro": "sub"
    }
    
    @staticmethod
    def get_style_for_scene(scene_name: str) -> str:
        """Obtiene el estilo de bajo recomendado para una scene."""
        scene_lower = scene_name.lower()
        for key, style in BassPatterns.SCENE_BASS_MAP.items():
            if key in scene_lower:
                return style
        return "sub"  # Default
    
    @staticmethod
    def get_bass_line(bars: int = 16, progression: List[str] = None, 
                      key: str = "A", style: str = "sub") -> List[NoteEvent]:
        """
        Genera lÃ­nea de bajo.
        
        ProgresiÃ³n: lista de nombres de acordes (ej: ["Am", "F", "C", "G"])
        Estilos:
        - sub: Sub-bajos largos y profundos
        - sustained: Notas sostenidas con release largo
        - pluck: Notas cortas y percusivas
        - slide: Con slides entre notas
        """
        notes = []
        
        if progression is None:
            # ProgresiÃ³n por defecto: vi-IV-I-V
            progression = ["Am", "F", "C", "G"]
        
        # Convertir acordes a notas raÃ­z (MIDI)
        root_notes = BassPatterns._chords_to_roots(progression, key)
        
        # DuraciÃ³n por acorde
        beats_per_chord = 4.0 * bars / len(progression)
        
        # Sprint 7: Soporte para los 8 estilos de bajo
        style_config = BassPatterns.BASS_STYLES.get(style, BassPatterns.BASS_STYLES["sub"])
        
        if style == "sub":
            # Sub-bajos: notas largas en raÃ­z
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                duration = beats_per_chord * 0.9
                pitch = root - 12  # Una octava abajo
                notes.append(NoteEvent(pitch, start, duration, 110))
                if i % 2 == 0:
                    fifth = pitch + 7
                    notes.append(NoteEvent(fifth, start + duration * 0.5, 0.25, 70))
                    
        elif style == "sustained":
            # Notas sostenidas con release
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                duration = beats_per_chord
                pitch = root - 12
                notes.append(NoteEvent(pitch, start, duration, 120))
                notes.append(NoteEvent(pitch + 12, start + 0.5, duration - 0.5, 90))
                
        elif style == "pluck":
            # Notas cortas y percusivas
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                pitch = root - 12
                notes.append(NoteEvent(pitch, start, 0.25, 115))
                notes.append(NoteEvent(pitch + 12, start + 0.5, 0.15, 100))
                notes.append(NoteEvent(pitch, start + beats_per_chord * 0.75, 0.2, 90))
                
        elif style == "slide":
            # Con slides/portamento entre notas
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                pitch = root - 12
                notes.append(NoteEvent(pitch, start, beats_per_chord * 0.8, 110))
                if i < len(root_notes) - 1:
                    next_pitch = root_notes[i + 1] - 12
                    slide_start = start + beats_per_chord * 0.8
                    slide_duration = beats_per_chord * 0.2
                    slide_note = pitch + 1 if next_pitch > pitch else pitch - 1
                    notes.append(NoteEvent(slide_note, slide_start, slide_duration, 80))
                    
        elif style == "slap":
            # Estilo slap con ataque fuerte
            for i, root in enumerate(root_notes):
                base_start = i * beats_per_chord
                pitch = root - 12
                pattern_offsets = style_config["pattern"]
                duration = style_config["duration"]
                for offset in pattern_offsets:
                    # Slap en el ataque (velocity alto)
                    notes.append(NoteEvent(pitch, base_start + offset, duration, 125))
                    # Ghost note despuÃ©s
                    if offset < 2.0:
                        notes.append(NoteEvent(pitch, base_start + offset + 0.1, 0.1, 70))
                        
        elif style == "octaves":
            # Alternando octavas
            for i, root in enumerate(root_notes):
                base_start = i * beats_per_chord
                pattern_offsets = style_config["pattern"]
                duration = style_config["duration"]
                for idx, offset in enumerate(pattern_offsets):
                    # Alternar entre octava baja y alta
                    pitch = root if idx % 2 == 0 else root + 12
                    notes.append(NoteEvent(pitch, base_start + offset, duration, 110))
                    
        elif style == "harmonics":
            # ArmÃ³nicos artificiales (simulado con octava alta y velocity moderado)
            for i, root in enumerate(root_notes):
                base_start = i * beats_per_chord
                pattern_offsets = style_config["pattern"]
                duration = style_config["duration"]
                for offset in pattern_offsets:
                    # Simular armÃ³nico con octava + 24 y velocity medio
                    harmonic_pitch = root + 24
                    notes.append(NoteEvent(harmonic_pitch, base_start + offset, duration, 95))
                    # TambiÃ©n nota raÃ­z muy corta para ataque
                    notes.append(NoteEvent(root, base_start + offset, 0.05, 80))
                    
        elif style == "synth":
            # Estilo sintetizador de onda
            for i, root in enumerate(root_notes):
                base_start = i * beats_per_chord
                pattern_offsets = style_config["pattern"]
                duration = style_config["duration"]
                for offset in pattern_offsets:
                    # Nota principal
                    notes.append(NoteEvent(root, base_start + offset, duration, 100))
                    # Quinta paralela para sonido mÃ¡s synth
                    fifth = root + 7
                    notes.append(NoteEvent(fifth, base_start + offset + 0.05, duration * 0.8, 85))
        else:
            # Default: sub
            for i, root in enumerate(root_notes):
                start = i * beats_per_chord
                duration = beats_per_chord * 0.9
                pitch = root - 12
                notes.append(NoteEvent(pitch, start, duration, 110))
                    
        return notes
    
    @staticmethod
    def _chords_to_roots(progression: List[str], key: str) -> List[int]:
        """Convierte nombres de acordes a notas MIDI raíz
        
        Args:
            progression: List of chord names (e.g., ["Am", "F", "C", "G"])
            key: Key with quality (e.g., "Am", "Cm", "F#m") - root note extracted automatically
        """
        # Notas base en octava 4 (C4 = 60)
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        
        # Extract root note from key (e.g., "Am" -> "A", "C#m" -> "C#")
        root_key = key.replace("m", "").replace("M", "") if key else "A"
        
        # Encontrar offset del key
        if root_key in note_names:
            key_offset = note_names.index(root_key)
        else:
            key_offset = 9  # Default A
            
        # C4 = 60, así que A3 = 57
        base_note = 57 + key_offset  # A3 por defecto si key=A
        
        # Intervalos para acordes (relativos a la tonalidad)
        roman_intervals = {
            "I": 0, "i": 0,
            "II": 2, "ii": 2,
            "III": 4, "iii": 4,
            "IV": 5, "iv": 5,
            "V": 7, "v": 7,
            "VI": 9, "vi": 9,
            "VII": 11, "vii": 11,
        }
        
        roots = []
        for chord in progression:
            # Extraer nota base del nombre del acorde
            if len(chord) >= 2 and chord[1] in ["#", "b"]:
                chord_root = chord[:2]
                quality = chord[2:]
            else:
                chord_root = chord[:1]
                quality = chord[1:]
                
            # Convertir a nÃºmero de nota
            if chord_root in note_names:
                root_num = note_names.index(chord_root)
            elif chord_root.upper() in roman_intervals:
                root_num = (base_note % 12 + roman_intervals[chord_root.upper()]) % 12
            else:
                root_num = base_note % 12
                
            # Construir nota MIDI completa (octava 3)
            midi_note = 48 + root_num  # C3 base
            if midi_note < base_note - 12:
                midi_note += 12
                
            roots.append(midi_note)
            
        return roots



class ChordProgressionsPro:
    """
    Sistema de progresiones armÃ³nicas profesional para reggaeton.
    Fases 41-50: 16 progresiones con tensiÃ³n, acordes extendidos, inversiones y anticipation.
    """
    
    # 16 PROGRESIONES con sistema de tensiÃ³n (Fases 41-45)
    PROGRESSIONS = {
        "intro": {"chords": ["vi", "IV", "I", "V"], "tension": [0.3, 0.2, 0.1, 0.4]},
        "verse_standard": {"chords": ["i", "v", "vi", "IV"], "tension": [0.2, 0.3, 0.2, 0.3]},
        "verse_alt1": {"chords": ["vi", "IV", "I", "V"], "tension": [0.3, 0.2, 0.1, 0.4]},
        "verse_alt2": {"chords": ["i", "VI", "III", "VII"], "tension": [0.2, 0.3, 0.4, 0.5]},
        "prechorus": {"chords": ["i", "iv", "VII", "VI"], "tension": [0.4, 0.5, 0.6, 0.7]},
        "chorus_power": {"chords": ["i", "V", "vi", "IV"], "tension": [0.2, 0.3, 0.2, 0.1]},
        "chorus_alternative": {"chords": ["i", "VII", "VI", "V"], "tension": [0.2, 0.4, 0.3, 0.6]},
        "chorus_rising": {"chords": ["i", "iv", "V", "I"], "tension": [0.3, 0.4, 0.6, 0.1]},
        "bridge_dark": {"chords": ["iv", "VII", "i", "VI"], "tension": [0.5, 0.6, 0.4, 0.5]},
        "outro_resolve": {"chords": ["i", "V", "i", "VII"], "tension": [0.2, 0.3, 0.1, 0.4]},
        "tense": {"chords": ["ii", "v", "i", "VII"], "tension": [0.6, 0.7, 0.4, 0.5]},
        "epic": {"chords": ["i", "VI", "iv", "V"], "tension": [0.2, 0.3, 0.4, 0.6]},
        "emotional": {"chords": ["vi", "I", "iii", "IV"], "tension": [0.4, 0.1, 0.5, 0.3]},
        "minimal": {"chords": ["i", "V", "i", "v"], "tension": [0.1, 0.3, 0.1, 0.4]},
        "modal_borrow": {"chords": ["i", "bVI", "bVII", "iv"], "tension": [0.2, 0.5, 0.4, 0.5]},
    }
    
    # ACORDES EXTENDIDOS (Fases 46-50)
    CHORD_EXTENSIONS = {
        "maj9": [0, 4, 7, 11, 14],     # 1, 3, 5, 7, 9
        "min9": [0, 3, 7, 10, 14],     # 1, b3, 5, b7, 9
        "dom9": [0, 4, 7, 10, 14],     # 1, 3, 5, b7, 9
        "sus4": [0, 5, 7],              # 1, 4, 5
        "7sus4": [0, 5, 7, 10],         # 1, 4, 5, b7
        "add9": [0, 4, 7, 14],          # 1, 3, 5, 9
        "maj11": [0, 4, 7, 11, 14, 17], # 1, 3, 5, 7, 9, 11
        "min11": [0, 3, 7, 10, 14, 17], # 1, b3, 5, b7, 9, 11
    }
    
    # Base voicings (triadas)
    BASE_VOICINGS = {
        "major": [0, 4, 7],      # 1, 3, 5
        "minor": [0, 3, 7],      # 1, b3, 5
        "dim": [0, 3, 6],        # 1, b3, b5
        "aug": [0, 4, 8],        # 1, 3, #5
        "maj7": [0, 4, 7, 11],   # 1, 3, 5, 7
        "min7": [0, 3, 7, 10],   # 1, b3, 5, b7
        "dom7": [0, 4, 7, 10],   # 1, 3, 5, b7
    }
    
    @staticmethod
    def get_progression(name):
        """
        Obtiene progresiÃ³n con datos de tensiÃ³n.
        
        Returns: Dict con chords, tension, avg_tension, max_tension
        """
        if name not in ChordProgressionsPro.PROGRESSIONS:
            raise ValueError("ProgresiÃ³n '%s' no existe. Disponibles: %s" % (name, list(ChordProgressionsPro.PROGRESSIONS.keys())))
        
        prog = ChordProgressionsPro.PROGRESSIONS[name]
        tensions = prog["tension"]
        return {
            "name": name,
            "chords": prog["chords"],
            "tension": tensions,
            "avg_tension": sum(tensions) / len(tensions),
            "max_tension": max(tensions),
            "min_tension": min(tensions),
        }
    
    @staticmethod
    def select_progression_for_section(section_type, energy_level=0.5):
        """
        Selecciona progresiÃ³n automÃ¡ticamente segÃºn tipo de secciÃ³n y energÃ­a.
        
        Args:
            section_type: "intro", "verse", "prechorus", "chorus", "bridge", "outro"
            energy_level: 0.0-1.0, nivel de energÃ­a deseado
        """
        section_map = {
            "intro": ["intro", "minimal", "emotional"],
            "verse": ["verse_standard", "verse_alt1", "verse_alt2", "minimal"],
            "prechorus": ["prechorus", "tense", "epic"],
            "chorus": ["chorus_power", "chorus_alternative", "chorus_rising", "epic"],
            "bridge": ["bridge_dark", "emotional", "modal_borrow"],
            "outro": ["outro_resolve", "minimal", "emotional"],
        }
        
        candidates = section_map.get(section_type.lower(), ["verse_standard"])
        
        # Seleccionar segÃºn energÃ­a
        if energy_level > 0.7:
            # Alta energÃ­a: progresiones con mÃ¡s tensiÃ³n
            return candidates[0] if candidates else "chorus_power"
        elif energy_level < 0.3:
            # Baja energÃ­a: progresiones con menos tensiÃ³n
            return candidates[-1] if candidates else "minimal"
        else:
            # Media: alternativa
            return candidates[1] if len(candidates) > 1 else candidates[0]
    
    @staticmethod
    def get_extended_chord(chord_name, extension=None, tension_level=0.0):
        """
        Obtiene notas para acorde con posible extensiÃ³n.
        
        Args:
            chord_name: Nombre del acorde (ej: "Am", "C", "G7")
            extension: Tipo de extensiÃ³n ("maj9", "min9", "dom9", "sus4", etc.)
            tension_level: 0.0-1.0, si es alto usa acordes extendidos automÃ¡ticamente
        """
        # Parsear nombre de acorde
        if len(chord_name) >= 2 and chord_name[1] in ["#", "b"]:
            root = chord_name[:2]
            quality = chord_name[2:].lower()
        else:
            root = chord_name[:1]
            quality = chord_name[1:].lower()
        
        # Determinar calidad base
        if "m" in quality and "maj" not in quality:
            base_quality = "minor"
        elif "maj" in quality or quality == "":
            base_quality = "major"
        elif "dim" in quality or quality == "Â°":
            base_quality = "dim"
        elif "aug" in quality or quality == "+":
            base_quality = "aug"
        elif "7" in quality and "maj7" not in quality:
            base_quality = "dom7"
        elif "maj7" in quality:
            base_quality = "maj7"
        else:
            base_quality = "minor"
        
        # Si hay tensiÃ³n alta (>0.6) y no hay extensiÃ³n explÃ­cita, usar extendido
        if extension is None and tension_level > 0.6:
            if base_quality in ["minor", "min7"]:
                extension = "min9"
            elif base_quality == "dom7":
                extension = "dom9"
            elif base_quality in ["major", "maj7"]:
                extension = "maj9"
        
        # Obtener intervalos
        if extension and extension in ChordProgressionsPro.CHORD_EXTENSIONS:
            intervals = ChordProgressionsPro.CHORD_EXTENSIONS[extension]
        else:
            intervals = ChordProgressionsPro.BASE_VOICINGS.get(
                base_quality, ChordProgressionsPro.BASE_VOICINGS["minor"]
            )
        
        return intervals
    
    @staticmethod
    def apply_inversion(notes, inversion=0):
        """
        Aplica inversiÃ³n a un acorde.
        
        Args:
            notes: Lista de notas MIDI del acorde
            inversion: 0 = posiciÃ³n fundamental, 1 = 1ra inversiÃ³n, 2 = 2da inversiÃ³n
        """
        if not notes or inversion == 0:
            return notes
        
        notes = sorted(notes)
        
        if inversion == 1 and len(notes) >= 2:
            # Primera inversiÃ³n: baja la fundamental una octava
            inverted = notes[1:] + [notes[0] + 12]
            return ChordProgressionsPro._optimize_voicing(inverted)
        elif inversion == 2 and len(notes) >= 3:
            # Segunda inversiÃ³n: baja la fundamental y 3ra una octava
            inverted = notes[2:] + [notes[0] + 12, notes[1] + 12]
            return ChordProgressionsPro._optimize_voicing(inverted)
        
        return notes
    
    @staticmethod
    def apply_chord_anticipation(start_time, anticipation=0.0625):
        """
        Aplica anticipaciÃ³n a un acorde (mover adelante del beat).
        Ãštil en Pre-Chorus para crear tensiÃ³n.
        
        Args:
            start_time: Tiempo de inicio original en beats
            anticipation: Cantidad de anticipaciÃ³n (default 1/16 = 0.0625 beats)
        """
        return max(0.0, start_time - anticipation)
    
    @staticmethod
    def _optimize_voicing(notes):
        """Optimiza voicing para que las notas estÃ©n cerca entre sÃ­"""
        if len(notes) <= 1:
            return notes
        
        result = [notes[0]]
        for note in notes[1:]:
            # Encontrar octava mÃ¡s cercana
            while note - result[-1] > 6:
                note -= 12
            while note - result[-1] < -6:
                note += 12
            result.append(note)
        
        return sorted(result)
    
    @staticmethod
    def get_all_progression_names():
        """Retorna todos los nombres de progresiones disponibles"""
        return list(ChordProgressionsPro.PROGRESSIONS.keys())


class ChordProgressions:
    """
    Progresiones de acordes estÃ¡ndar para reggaeton.
    """
    
    # Progresiones predefinidas (notas como nÃºmeros romanos o nombres)
    PROGRESSIONS = {
        "vi-IV-I-V": ["Am", "F", "C", "G"],
        "i-VI-VII": ["Am", "F", "G"],
        "i-iv-VII-VI": ["Am", "Dm", "G", "F"],
        "i-VI-III-VII": ["Am", "F", "C", "G"],
        "ii-V-I": ["Dm", "G", "C"],
        "I-V-vi-IV": ["C", "G", "Am", "F"],
        "vi-V-IV-III": ["Am", "G", "F", "E"],
        "i-VII-VI-VII": ["Am", "G", "F", "G"],  # Muy comÃºn en reggaeton
    }
    
    # Estructuras de acordes (triadas + extendidos)
    CHORD_VOICINGS = {
        "major": [0, 4, 7],      # 1, 3, 5
        "minor": [0, 3, 7],      # 1, b3, 5
        "dim": [0, 3, 6],        # 1, b3, b5
        "aug": [0, 4, 8],        # 1, 3, #5
        "maj7": [0, 4, 7, 11],   # 1, 3, 5, 7
        "min7": [0, 3, 7, 10],   # 1, b3, 5, b7
        "dom7": [0, 4, 7, 10],   # 1, 3, 5, b7
        "sus4": [0, 5, 7],       # 1, 4, 5
        # Acordes extendidos (Fases 46-50)
        "maj9": [0, 4, 7, 11, 14],     # 1, 3, 5, 7, 9
        "min9": [0, 3, 7, 10, 14],     # 1, b3, 5, b7, 9
        "dom9": [0, 4, 7, 10, 14],     # 1, 3, 5, b7, 9
        "add9": [0, 4, 7, 14],          # 1, 3, 5, 9
        "7sus4": [0, 5, 7, 10],         # 1, 4, 5, b7
    }
    
    @staticmethod
    def get_progression(name: str, key: str = "Am", bars: int = 16) -> List[Dict[str, Any]]:
        """
        Obtiene progresión de acordes con timing.
        
        Retorna lista de dicts con: chord_name, root_pitch, notes, start_beat, duration
        key: Key with quality (e.g., "Am", "Cm", "F#m") - root note extracted automatically
        """
        if name in ChordProgressions.PROGRESSIONS:
            chord_names = ChordProgressions.PROGRESSIONS[name]
        else:
            chord_names = name.split("-")
        
        # Convertir a notas
        result = []
        beats_per_chord = 4.0 * bars / len(chord_names)
        
        # Extract root note from key (e.g., "Am" -> "A", "C#m" -> "C#")
        root_key = key.replace("m", "").replace("M", "") if key else "A"
        
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        key_offset = note_names.index(root_key) if root_key in note_names else 9  # Default A
        base_note = 57  # A3
        
        for i, chord_name in enumerate(chord_names):
            # Parsear nombre de acorde
            if len(chord_name) >= 2 and chord_name[1] in ["#", "b"]:
                root_name = chord_name[:2]
                quality = chord_name[2:]
            else:
                root_name = chord_name[:1]
                quality = chord_name[1:]
                
            # Encontrar nota raÃ­z
            if root_name in note_names:
                root_num = note_names.index(root_name)
            else:
                root_num = key_offset
                
            # Ajustar a octava apropiada
            root_pitch = 48 + root_num  # C3 base
            if root_pitch < base_note - 12:
                root_pitch += 12
                
            # Determinar calidad
            if quality in ["m", "min", "minor", "-"]:
                voicing = "min7"
            elif quality in ["7", "dom"]:
                voicing = "dom7"
            elif quality in ["maj7", "M7"]:
                voicing = "maj7"
            elif quality == "sus4":
                voicing = "sus4"
            elif quality in ["dim", "Â°"]:
                voicing = "dim"
            else:
                voicing = "min7" if "m" in quality else "dom7"
                
            # Construir notas del acorde
            intervals = ChordProgressions.CHORD_VOICINGS.get(voicing, ChordProgressions.CHORD_VOICINGS["minor"])
            chord_notes = [root_pitch + interval for interval in intervals]
            
            # Voicing en posiciÃ³n cercana (inversiones)
            chord_notes = ChordProgressions._optimize_voicing(chord_notes)
            
            result.append({
                "chord_name": chord_name,
                "root_pitch": root_pitch,
                "notes": chord_notes,
                "start_beat": i * beats_per_chord,
                "duration": beats_per_chord,
                "voicing": voicing
            })
            
        return result
    
    @staticmethod
    def _optimize_voicing(notes: List[int]) -> List[int]:
        """Optimiza voicing para que las notas estÃ©n cerca entre sÃ­"""
        if len(notes) <= 1:
            return notes
            
        # Asegurar que todas las notas estÃ©n en un rango de una octava
        result = [notes[0]]
        for note in notes[1:]:
            # Encontrar octava mÃ¡s cercana
            while note - result[-1] > 6:
                note -= 12
            while note - result[-1] < -6:
                note += 12
            result.append(note)
            
        return sorted(result)
    
    @staticmethod
    def get_all_progression_names() -> List[str]:
        """Retorna todos los nombres de progresiones disponibles"""
        return list(ChordProgressions.PROGRESSIONS.keys())


class MelodyGenerator:
    """
    Generador de melodÃ­as para reggaeton.
    Sprint 7: Agregado generate_counter_melody y generate_arpeggio.
    """
    
    # Escalas (intervalos semitonos)
    SCALES = {
        "minor": [0, 2, 3, 5, 7, 8, 10],           # Natural minor
        "major": [0, 2, 4, 5, 7, 9, 11],           # Major
        "pentatonic_minor": [0, 3, 5, 7, 10],     # Pentatonic minor
        "pentatonic_major": [0, 2, 4, 7, 9],      # Pentatonic major
        "blues": [0, 3, 5, 6, 7, 10],              # Blues scale
        "dorian": [0, 2, 3, 5, 7, 9, 10],          # Dorian mode
        "phrygian": [0, 1, 3, 5, 7, 8, 10],        # Phrygian mode
        "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], # Harmonic minor
    }
    
    @staticmethod
    def generate_melody(bars: int = 16, scale: str = "minor", 
                        density: float = 0.5, key: str = "Am") -> List[NoteEvent]:
        """
        Genera melodía automáticamente.
        
        density: 0.0-1.0, probabilidad de nota por subdivisión
        key: Key with quality (e.g., "Am", "C", "Gm") - root note extracted automatically
        """
        notes = []
        
        # Extract root note from key (e.g., "Am" -> "A", "C#m" -> "C#")
        root_key = key.replace("m", "").replace("M", "") if key else "A"
        
        # Obtener escala
        if scale in MelodyGenerator.SCALES:
            intervals = MelodyGenerator.SCALES[scale]
        else:
            intervals = MelodyGenerator.SCALES["minor"]
            
        # Encontrar nota raíz
        note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
        key_offset = note_names.index(root_key) if root_key in note_names else 9
        root_pitch = 60 + key_offset  # C4 base
        
        # Generar notas disponibles (2 octavas)
        available_notes = []
        for octave in [0, 1]:  # 2 octavas
            for interval in intervals:
                available_notes.append(root_pitch + interval + (octave * 12))
                
        # Subdivisiones por compÃ¡s segÃºn densidad
        if density < 0.3:
            subdivisions = 4  # Negras
        elif density < 0.6:
            subdivisions = 8  # Corcheas
        else:
            subdivisions = 16  # Semicorcheas
            
        subdivision_duration = 4.0 / subdivisions
        
        # Generar notas
        for bar in range(bars):
            bar_offset = bar * 4.0
            
            for sub in range(subdivisions):
                if random.random() < density:
                    start_time = bar_offset + (sub * subdivision_duration)
                    
                    # Seleccionar nota (preferir notas de acorde: 1, 3, 5)
                    if random.random() < 0.7:
                        # Nota de acorde (1, 3, 5)
                        degree = random.choice([0, 2, 4])  # Ãndices en escala
                        octave = random.choice([0, 1])
                        pitch = root_pitch + intervals[degree] + (octave * 12)
                    else:
                        # Cualquier nota de la escala
                        pitch = random.choice(available_notes)
                        
                    # DuraciÃ³n segÃºn posiciÃ³n
                    if sub % 4 == 0:  # Tiempo fuerte
                        duration = subdivision_duration * 2
                        velocity = 110
                    elif sub % 2 == 0:  # Semi-fuerte
                        duration = subdivision_duration * 1.5
                        velocity = 100
                    else:  # DÃ©bil
                        duration = subdivision_duration
                        velocity = 90
                        
                    notes.append(NoteEvent(pitch, start_time, duration, velocity))
                    
        # Ordenar por tiempo
        notes.sort(key=lambda n: n.start_time)
        
        # Asegurar que no haya superposiciones excesivas
        notes = MelodyGenerator._clean_overlaps(notes)
        
        return notes
    
    @staticmethod
    def _clean_overlaps(notes: List[NoteEvent]) -> List[NoteEvent]:
        """Limpia superposiciones de notas en el mismo pitch"""
        if not notes:
            return notes
            
        # Agrupar por pitch
        by_pitch = {}
        for note in notes:
            if note.pitch not in by_pitch:
                by_pitch[note.pitch] = []
            by_pitch[note.pitch].append(note)
            
        # Limpiar cada grupo
        cleaned = []
        for pitch, pitch_notes in by_pitch.items():
            pitch_notes.sort(key=lambda n: n.start_time)
            
            for i, note in enumerate(pitch_notes):
                if i > 0:
                    prev = pitch_notes[i - 1]
                    # Si se superpone, acortar la anterior
                    if prev.start_time + prev.duration > note.start_time:
                        prev.duration = note.start_time - prev.start_time
                        
            cleaned.extend(pitch_notes)
            
        # Re-ordenar
        cleaned.sort(key=lambda n: n.start_time)
        return cleaned
    
    @staticmethod
    def generate_counter_melody(main_melody: List[NoteEvent], scale: str = "minor",
                                 interval: int = 3, timing_offset: float = 0.25,
                                 velocity_reduction: float = 0.20) -> List[NoteEvent]:
        """
        Sprint 7 - Fase 72: Genera contramelodÃ­a a partir de melodÃ­a principal.
        
        Args:
            main_melody: Lista de NoteEvent de la melodÃ­a principal
            interval: Intervalo de contrapunto (3 = tercera, 6 = sexta, -3 = tercera abajo)
            timing_offset: Desplazamiento de timing en beats (default 0.25)
            velocity_reduction: ReducciÃ³n de velocity como fracciÃ³n (default 0.20 = -20%)
        
        Returns:
            Lista de NoteEvent para la contramelodÃ­a
        """
        counter_notes = []
        
        for note in main_melody:
            # Calcular pitch a intervalo especificado
            counter_pitch = note.pitch + interval
            
            # Ajustar a escala si es necesario
            intervals = MelodyGenerator.SCALES.get(scale, MelodyGenerator.SCALES["minor"])
            root = note.pitch % 12
            target = counter_pitch % 12
            
            # Verificar si estÃ¡ en escala
            scale_notes = [(root + i) % 12 for i in intervals]
            if target not in scale_notes:
                # Ajustar al grado mÃ¡s cercano
                closest = min(scale_notes, key=lambda x: abs(x - target))
                counter_pitch = note.pitch + (closest - root) + (12 if interval > 0 else -12)
                
            # Timing desplazado + velocity reducida
            velocity = int(note.velocity * (1.0 - velocity_reduction))
            counter_notes.append(NoteEvent(
                counter_pitch,
                note.start_time + timing_offset,
                note.duration * 0.7,
                max(1, min(127, velocity))
            ))
            
        return counter_notes
    
    @staticmethod
    def generate_arpeggio(chord_notes: List[int], pattern: str = "up", 
                          duration: float = 4.0, velocity: int = 100) -> List[NoteEvent]:
        """
        Sprint 7 - Fase 73: Genera arpegio a partir de notas de acorde.
        
        Args:
            chord_notes: Lista de pitches MIDI del acorde (ej: [60, 64, 67] para C major)
            pattern: PatrÃ³n de arpegio - "up", "down", "updown", "random"
            duration: DuraciÃ³n total del arpegio en beats
            velocity: Velocity base para las notas
        
        Returns:
            Lista de NoteEvent para el arpegio
        """
        if not chord_notes:
            return []
        
        arpeggio_notes = []
        note_count = len(chord_notes)
        
        # Determinar orden segÃºn patrÃ³n
        if pattern == "up":
            order = list(range(note_count))
        elif pattern == "down":
            order = list(range(note_count - 1, -1, -1))
        elif pattern == "updown":
            order = list(range(note_count)) + list(range(note_count - 2, 0, -1))
        elif pattern == "random":
            order = [random.randint(0, note_count - 1) for _ in range(note_count * 2)]
        else:
            order = list(range(note_count))
        
        # Calcular duraciÃ³n por nota
        note_duration = duration / len(order)
        
        for i, idx in enumerate(order):
            pitch = chord_notes[idx % note_count]
            start_time = i * note_duration
            # AÃ±adir variaciÃ³n de velocity
            vel = velocity + random.randint(-10, 10)
            arpeggio_notes.append(NoteEvent(
                pitch, start_time, note_duration * 0.8, max(1, min(127, vel))
            ))
        
        return arpeggio_notes


class HumanFeel:
    """
    SPRINT 7: Sistema completo de humanizaciÃ³n y groove para AbletonMCP_AI.
    
    CaracterÃ­sticas implementadas:
    - 10 perfiles de humanizaciÃ³n por tipo de instrumento (Fases 26-30)
    - Micro-timing por nivel de energÃ­a (Fases 31-35)
    - Velocity scaling por secciÃ³n (Fases 36-40)
    - Live drummer feel (Fases 41-45): push/pull, ghost notes, hi-hat splash
    """
    
    # SPRINT 7 - FASES 26-30: 10 PERFILES DE HUMANIZACIÃ“N
    HUMANIZATION_PROFILES = {
        "kick": {
            "timing_ms": 5, 
            "velocity": 15, 
            "length_percent": 5,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "snare": {
            "timing_ms": 10, 
            "velocity": 20, 
            "length_percent": 8,
            "swing": 0.0,
            "ghost_notes": True,
            "pitch_drift": False,
            "creative": False
        },
        "hihat": {
            "timing_ms": 15, 
            "velocity": 30, 
            "length_percent": 10,
            "swing": 0.6,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "bass": {
            "timing_ms": 8, 
            "velocity": 12, 
            "length_percent": 6,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "chords": {
            "timing_ms": 12, 
            "velocity": 18, 
            "length_percent": 8,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "lead": {
            "timing_ms": 12, 
            "velocity": 18, 
            "length_percent": 10,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": True,
            "creative": False
        },
        "pad": {
            "timing_ms": 5, 
            "velocity": 10, 
            "length_percent": 3,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "perc": {
            "timing_ms": 15, 
            "velocity": 25, 
            "length_percent": 10,
            "swing": 0.5,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        },
        "fx": {
            "timing_ms": 20, 
            "velocity": 20, 
            "length_percent": 15,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": True,
            "creative": True
        },
        "stabs": {
            "timing_ms": 10, 
            "velocity": 15, 
            "length_percent": 5,
            "swing": 0.0,
            "ghost_notes": False,
            "pitch_drift": False,
            "creative": False
        }
    }
    
    # SPRINT 7 - FASES 36-40: VELOCITY SCALING POR SECCIÃ“N
    SECTION_VELOCITY_RANGES = {
        "intro": (50, 70),
        "verse": (60, 85),
        "pre_chorus": (75, 95),
        "chorus": (90, 127),
        "bridge": (70, 100),
        "build_up": (80, 110),
        "outro": (60, 80)
    }
    
    @staticmethod
    def get_profile_for_track(track_name: str) -> Dict[str, Any]:
        """Detecta el perfil de humanizaciÃ³n basado en el nombre del track."""
        name_lower = track_name.lower() if track_name else ""
        
        profile_map = [
            ("kick", "kick"), ("bombo", "kick"),
            ("snare", "snare"), ("caja", "snare"), ("clap", "snare"), ("palma", "snare"),
            ("hat", "hihat"), ("hihat", "hihat"), ("hi-hat", "hihat"),
            ("bass", "bass"), ("bajo", "bass"), ("sub", "bass"),
            ("chord", "chords"), ("acorde", "chords"), ("pad", "chords"), ("harmony", "chords"),
            ("lead", "lead"), ("melody", "lead"), ("melodia", "lead"), ("solo", "lead"),
            ("vocal", "lead"), ("voice", "lead"), ("synth", "lead"),
            ("pluck", "stabs"), ("stab", "stabs"),
            ("perc", "perc"), ("conga", "perc"), ("timbal", "perc"), ("shaker", "perc"),
            ("guiro", "perc"), ("maracas", "perc"),
            ("fx", "fx"), ("effect", "fx"), ("riser", "fx"), ("sweep", "fx"),
            ("impact", "fx"), ("downlifter", "fx"),
        ]
        
        for keyword, profile_name in profile_map:
            if keyword in name_lower:
                return HumanFeel.HUMANIZATION_PROFILES.get(profile_name, HumanFeel.HUMANIZATION_PROFILES["kick"])
        
        return HumanFeel.HUMANIZATION_PROFILES["kick"]
    
    # FUNCIONES BASE DE HUMANIZACIÃ“N
    
    @staticmethod
    def apply_micro_timing(notes: List[NoteEvent], variance_ms: float = 15, bpm: float = None) -> List[NoteEvent]:
        """Ajusta timing de notas Â±variance_ms milisegundos."""
        if bpm is None:
            bpm = 95.0
        
        beat_duration_ms = 60000.0 / bpm
        variance_beats = variance_ms / beat_duration_ms
        
        result = []
        for note in notes:
            new_note = note.copy()
            offset = random.gauss(0, variance_beats)
            new_note.start_time += offset
            new_note.start_time = max(0, new_note.start_time)
            result.append(new_note)
        return result
    
    @staticmethod
    def apply_velocity_variation(notes: List[NoteEvent], variance: int = 10) -> List[NoteEvent]:
        """Aplica variaciÃ³n de velocidad Â±variance."""
        result = []
        for note in notes:
            new_note = note.copy()
            vel_change = random.randint(-variance, variance)
            new_note.velocity = max(1, min(127, note.velocity + vel_change))
            result.append(new_note)
        return result
    
    @staticmethod
    def apply_length_variation(notes: List[NoteEvent], variance_percent: float = 5.0) -> List[NoteEvent]:
        """Aplica variaciÃ³n de duraciÃ³n Â±variance_percent%."""
        result = []
        variance_decimal = variance_percent / 100.0
        
        for note in notes:
            new_note = note.copy()
            factor = 1.0 + random.uniform(-variance_decimal, variance_decimal)
            new_note.duration = max(0.01, note.duration * factor)
            result.append(new_note)
        return result
    
    @staticmethod
    def apply_all_humanization(notes: List[NoteEvent], 
                                timing_variance_ms: float = 15,
                                velocity_variance: int = 10,
                                length_variance_percent: float = 5.0,
                                bpm: float = None) -> List[NoteEvent]:
        """Aplica todas las humanizaciones base en secuencia."""
        result = HumanFeel.apply_micro_timing(notes, timing_variance_ms, bpm)
        result = HumanFeel.apply_velocity_variation(result, velocity_variance)
        result = HumanFeel.apply_length_variation(result, length_variance_percent)
        return result
    
    # SPRINT 7 - FASES 31-35: MICRO-TIMING POR ENERGÃA
    
    @staticmethod
    def apply_micro_timing_by_energy(notes: List[NoteEvent], base_variance_ms: float = 15, 
                                      energy_level: float = 0.5, bpm: float = None) -> List[NoteEvent]:
        """
        Micro-timing ajustado por nivel de energÃ­a.
        - Baja energÃ­a (0.2-0.4): mÃ¡s timing variance (+20%)
        - Alta energÃ­a (0.8-1.0): timing mÃ¡s tight (-30%)
        """
        if bpm is None:
            bpm = 95.0
            
        if energy_level < 0.4:
            variance_adjustment = 1.2
        elif energy_level > 0.8:
            variance_adjustment = 0.7
        else:
            variance_adjustment = 1.0
            
        adjusted_variance = base_variance_ms * variance_adjustment
        return HumanFeel.apply_micro_timing(notes, adjusted_variance_ms, bpm)
    
    # SPRINT 7 - FASES 36-40: VELOCITY SCALING POR SECCIÃ“N
    
    @staticmethod
    def apply_velocity_by_section(notes: List[NoteEvent], section_type: str = "verse",
                                   crescendo: bool = False, decrescendo: bool = False) -> List[NoteEvent]:
        """
        Velocity scaling por tipo de secciÃ³n:
        - Intro: 50-70
        - Chorus: 90-127
        - Build Up: crescendo +20 durante clip
        - Outro: fade a 30%
        """
        if not notes:
            return notes
            
        velocity_range = HumanFeel.SECTION_VELOCITY_RANGES.get(section_type, (60, 90))
        min_vel, max_vel = velocity_range
        
        max_time = max(note.start_time for note in notes) if notes else 0.0
        clip_duration = max(max_time, 4.0)
        
        result = []
        for note in notes:
            new_note = note.copy()
            original_vel = note.velocity
            
            normalized = original_vel / 127.0
            scaled_vel = min_vel + (normalized * (max_vel - min_vel))
            
            if crescendo and section_type == "build_up":
                position_factor = note.start_time / clip_duration if clip_duration > 0 else 0
                crescendo_boost = position_factor * 20
                scaled_vel = min(127, scaled_vel + crescendo_boost)
            
            if decrescendo and section_type == "outro":
                position_factor = note.start_time / clip_duration if clip_duration > 0 else 0
                fade_target = original_vel * 0.3
                fade_amount = (original_vel - fade_target) * position_factor
                scaled_vel = max(0, original_vel - fade_amount)
            
            new_note.velocity = max(0, min(127, int(scaled_vel)))
            result.append(new_note)
        return result
    
    # SPRINT 7 - FASES 41-45: LIVE DRUMMER FEEL
    
    @staticmethod
    def apply_live_drummer_feel(notes: List[NoteEvent], intensity: float = 0.5,
                                 enable_push_pull: bool = True,
                                 enable_ghost_notes: bool = True,
                                 enable_hi_hat_splash: bool = True) -> List[NoteEvent]:
        """
        Efectos de baterista humano real:
        - Push/pull: adelantar acentos, retrasar off-beats
        - 1% probabilidad de omitir notas muy dÃ©biles
        - Hi-hat foot splash al final de frases
        """
        if not notes:
            return notes
            
        result = []
        notes_to_remove = []
        
        PUSH_PULL_AMOUNT = 0.02 * intensity
        GHOST_NOTE_THRESHOLD = 40
        OMIT_CHANCE = 0.01
        
        phrase_length = 16.0
        max_time = max(note.start_time for note in notes)
        
        for note in notes:
            new_note = note.copy()
            
            # Push/Pull timing
            if enable_push_pull:
                beat_position = note.start_time % 4.0
                
                if beat_position < 0.1 or (beat_position > 0.9 and beat_position < 1.1):
                    # Downbeat - push
                    new_note.start_time -= PUSH_PULL_AMOUNT * random.uniform(0.5, 1.0)
                elif beat_position > 1.9 and beat_position < 2.1:
                    # Beat 2 - pull
                    new_note.start_time += PUSH_PULL_AMOUNT * random.uniform(0.3, 0.7)
                elif beat_position > 2.4 and beat_position < 2.6:
                    # Snare dembow (2.25) - pull
                    new_note.start_time += PUSH_PULL_AMOUNT * random.uniform(0.5, 1.0)
                else:
                    # Off-beats - pull sutil
                    new_note.start_time += PUSH_PULL_AMOUNT * random.uniform(0.1, 0.4)
            
            # Omitir notas dÃ©biles (1% chance)
            if note.velocity < GHOST_NOTE_THRESHOLD:
                if random.random() < OMIT_CHANCE:
                    notes_to_remove.append(note)
                    continue
            
            # Refinamiento de ghost notes
            if enable_ghost_notes and note.velocity < 60:
                new_note.start_time += random.gauss(0, 0.015 * intensity)
                new_note.velocity = max(1, new_note.velocity + random.randint(-5, 5))
            
            result.append(new_note)
        
        # Hi-hat foot splash al final de frases
        if enable_hi_hat_splash and intensity > 0.3:
            phrase_positions = []
            current_phrase = 0
            while current_phrase <= max_time:
                phrase_end = current_phrase + phrase_length - 0.5
                if phrase_end <= max_time:
                    phrase_positions.append(phrase_end)
                current_phrase += phrase_length
            
            for splash_pos in phrase_positions:
                has_nearby = any(abs(splash_pos - n.start_time) < 0.2 for n in notes)
                if not has_nearby:
                    splash = NoteEvent(
                        pitch=42,  # Closed hi-hat
                        start_time=splash_pos,
                        duration=0.1,
                        velocity=100 + random.randint(-10, 10)
                    )
                    result.append(splash)
        
        # Eliminar notas omitidas
        result = [n for n in result if n not in notes_to_remove]
        result.sort(key=lambda n: n.start_time)
        return result
    
    # FUNCIONES AUXILIARES
    
    @staticmethod
    def apply_swing(notes: List[NoteEvent], swing_amount: float = 0.6) -> List[NoteEvent]:
        """Aplica swing retrasando los off-beats."""
        if swing_amount <= 0.5:
            return [n.copy() for n in notes]
            
        swing_factor = (swing_amount - 0.5) * 0.5
        
        result = []
        for note in notes:
            new_note = note.copy()
            beat_position = note.start_time % 1.0
            if beat_position >= 0.25 and beat_position < 0.75:
                new_note.start_time += swing_factor
            result.append(new_note)
        return result
    
    @staticmethod
    def apply_pitch_drift(notes: List[NoteEvent], intensity: float = 0.5) -> List[NoteEvent]:
        """Simula pitch drift via variaciÃ³n de velocity."""
        result = []
        for note in notes:
            new_note = note.copy()
            drift = random.gauss(0, 5 * intensity)
            new_note.velocity = max(1, min(127, int(note.velocity + drift)))
            result.append(new_note)
        return result
    
    @staticmethod
    def apply_timing_bias(notes: List[NoteEvent], bias: str = "lay_back", bpm: float = None) -> List[NoteEvent]:
        """Aplica sesgo de timing al compÃ¡s."""
        if bpm is None:
            bpm = 95.0
        
        beat_duration_ms = 60000.0 / bpm
        
        if bias == "lay_back":
            offset_ms = random.uniform(10, 20)
        elif bias == "ahead":
            offset_ms = random.uniform(-20, -10)
        else:
            return [n.copy() for n in notes]
            
        offset_beats = offset_ms / beat_duration_ms
        
        result = []
        for note in notes:
            new_note = note.copy()
            new_note.start_time += offset_beats
            new_note.start_time = max(0, new_note.start_time)
            result.append(new_note)
        return result
    
    # SPRINT 7: FUNCIÃ“N PRINCIPAL - HUMANIZACIÃ“N COMPLETA
    
    @staticmethod
    def apply_complete_humanization(notes: List[NoteEvent],
                                     track_name: str = "",
                                     section_type: str = "verse",
                                     energy_level: float = 0.5,
                                     intensity: float = 0.5,
                                     bpm: float = None) -> List[NoteEvent]:
        """
        HumanizaciÃ³n completa usando todos los sistemas del Sprint 7.
        
        Combina: perfil por instrumento, micro-timing por energÃ­a,
        velocity scaling por secciÃ³n, y live drummer feel.
        """
        if not notes:
            return notes
            
        # 1. Obtener perfil
        profile = HumanFeel.get_profile_for_track(track_name)
        
        # 2. Micro-timing por energÃ­a
        base_timing = profile["timing_ms"] * intensity
        notes = HumanFeel.apply_micro_timing_by_energy(
            notes, 
            base_variance_ms=base_timing,
            energy_level=energy_level,
            bpm=bpm
        )
        
        # 3. Velocity scaling por secciÃ³n
        is_crescendo = section_type == "build_up"
        is_decrescendo = section_type == "outro"
        notes = HumanFeel.apply_velocity_by_section(
            notes,
            section_type=section_type,
            crescendo=is_crescendo,
            decrescendo=is_decrescendo
        )
        
        # 4. VariaciÃ³n de duraciÃ³n
        notes = HumanFeel.apply_length_variation(
            notes,
            variance_percent=profile["length_percent"] * intensity
        )
        
        # 5. VariaciÃ³n de velocity del perfil
        notes = HumanFeel.apply_velocity_variation(
            notes,
            variance=int(profile["velocity"] * intensity)
        )
        
        # 6. Live drummer feel (para drums)
        is_drum_track = any(keyword in track_name.lower() 
                           for keyword in ["kick", "snare", "drum", "perc", "hat", "bombo", "caja"])
        if is_drum_track:
            notes = HumanFeel.apply_live_drummer_feel(
                notes,
                intensity=intensity,
                enable_push_pull=True,
                enable_ghost_notes=profile.get("ghost_notes", False),
                enable_hi_hat_splash=True
            )
        
        # 7. Swing
        if profile.get("swing", 0) > 0:
            notes = HumanFeel.apply_swing(notes, swing_amount=profile["swing"] * intensity)
        
        # 8. Pitch drift
        if profile.get("pitch_drift", False):
            notes = HumanFeel.apply_pitch_drift(notes, intensity=intensity)
        
        return notes


class PercussionLibrary:
    """
    LibrerÃ­a de percusiones adicionales y efectos para reggaeton.
    Sprint 7: Agregado fills de drums, snare rolls, y vocal chops.
    """
    
    # Notas MIDI para percusiÃ³n
    PERCUSSION_NOTES = {
        "timbal": 47,       # High floor tom
        "conga_low": 48,    # High tom
        "conga_mid": 50,    # High tom 2
        "conga_high": 45,   # Low tom
        "bongo_low": 60,    # High bongo
        "bongo_high": 61,   # Low bongo
        "claves": 75,       # Claves
        "guiro": 73,        # Short guiro
        "guiro_long": 74,   # Long guiro
        "maracas": 70,      # Maracas
        "cabasa": 69,       # Cabasa
        "tambourine": 54,   # Tambourine
        "agogo": 67,        # High agogo
        "whistle": 72,      # Whistle
        "triangle": 80,     # Triangle
        "shaker": 82,       # Shaker
        "timbale": 65,      # High timbale
        "timbale_low": 66,  # Low timbale
    }
    
    FX_NOTES = {
        "riser": 93,        # Efecto de subida
        "downer": 91,       # Efecto de bajada
        "sweep": 92,        # Sweep
        "impact": 94,       # Impacto
        "crash": 49,        # Crash cymbal
        "reverse_crash": 55,# Reverse cymbal
        "fx_hit": 95,       # Hit FX
        "noise": 96,        # Noise burst
        "sub_drop": 97,     # Sub drop
        "tape_stop": 98,    # Tape stop effect
    }
    
    @staticmethod
    def get_percussion_fill(bars: int = 4, intensity: float = 0.7) -> List[NoteEvent]:
        """
        Genera fill de percusiÃ³n latina.
        
        intensity: 0.0-1.0, densidad del fill
        """
        notes = []
        
        # Instrumentos a usar segÃºn intensidad
        instruments = ["conga_mid", "conga_high", "timbale"]
        if intensity > 0.5:
            instruments.extend(["timbal", "bongo_high"])
        if intensity > 0.7:
            instruments.append("claves")
            
        # PatrÃ³n de fills tÃ­pico de reggaeton
        fill_patterns = [
            # PatrÃ³n 1: Roll descendente
            [(0, "conga_high"), (0.25, "conga_mid"), (0.5, "conga_low"), (0.75, "timbale")],
            # PatrÃ³n 2: Alternado
            [(0, "conga_mid"), (0.125, "timbale"), (0.25, "conga_mid"), (0.375, "timbale"),
             (0.5, "conga_high"), (0.75, "conga_mid")],
            # PatrÃ³n 3: Tumbao
            [(0, "conga_low"), (0.5, "conga_mid"), (0.75, "conga_high"), (0.875, "conga_mid")],
        ]
        
        pattern = random.choice(fill_patterns)
        
        # Generar notas del fill
        for bar_offset_mul in range(bars):
            bar_offset = bar_offset_mul * 4.0
            
            for time_offset, instrument in pattern:
                start = bar_offset + time_offset
                pitch = PercussionLibrary.PERCUSSION_NOTES.get(instrument, 60)
                
                # Velocidad segÃºn intensidad
                base_vel = 80 + int(intensity * 40)
                velocity = min(127, base_vel + random.randint(-10, 10))
                
                notes.append(NoteEvent(pitch, start, 0.15, velocity))
                
        return notes
    
    @staticmethod
    def get_fx_hit(position: float, fx_type: str = "riser", duration: float = 2.0) -> NoteEvent:
        """
        Genera un efecto FX en posiciÃ³n especÃ­fica.
        
        position: tiempo en beats
        fx_type: "riser", "downer", "impact", "crash", "sweep"
        duration: duraciÃ³n del FX en beats
        """
        pitch = PercussionLibrary.FX_NOTES.get(fx_type, 93)
        velocity = 110 if fx_type in ["impact", "crash"] else 100
        
        return NoteEvent(pitch, position, duration, velocity)
    
    @staticmethod
    def get_intro_buildup(bars: int = 4) -> List[NoteEvent]:
        """
        Genera buildup para intro (subida de tensiÃ³n).
        """
        notes = []
        
        # Cada vez mÃ¡s denso
        for bar in range(bars):
            bar_offset = bar * 4.0
            density = (bar + 1) / bars  # 0.25, 0.5, 0.75, 1.0
            
            # Shaker cada vez mÃ¡s rÃ¡pido
            subdivisions = int(4 + (density * 12))  # 4 a 16
            for i in range(subdivisions):
                start = bar_offset + (i * (4.0 / subdivisions))
                vel = 60 + int(density * 60)  # Crescendo
                notes.append(NoteEvent(
                    PercussionLibrary.PERCUSSION_NOTES["shaker"],
                    start, 0.05, min(127, vel)
                ))
                
        # Riser final
        notes.append(PercussionLibrary.get_fx_hit(bars * 4.0 - 2.0, "riser", 2.0))
        
        return notes
    
    @staticmethod
    def get_transition_fill(position: float, type: str = "break") -> List[NoteEvent]:
        """
        Genera fill de transiciÃ³n.
        
        type: "break", "build", "drop", "impact"
        """
        notes = []
        
        if type == "break":
            # Silencio seguido de impacto
            notes.append(PercussionLibrary.get_fx_hit(position + 0.5, "reverse_crash", 1.0))
            notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "impact", 0.5))
            
        elif type == "build":
            # Build con congas
            for i in range(8):
                start = position + (i * 0.125)
                notes.append(NoteEvent(
                    PercussionLibrary.PERCUSSION_NOTES["conga_mid"],
                    start, 0.1, 80 + i * 5
                ))
            notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "sweep", 0.5))
            
        elif type == "drop":
            # Drop con sub
            notes.append(PercussionLibrary.get_fx_hit(position, "sub_drop", 1.0))
            notes.append(PercussionLibrary.get_fx_hit(position, "crash", 1.0))
            
        elif type == "impact":
            # Impacto fuerte
            notes.append(PercussionLibrary.get_fx_hit(position, "impact", 0.8))
            notes.append(NoteEvent(
                PercussionLibrary.FX_NOTES["crash"],
                position, 1.0, 127
            ))
            
        return notes
    
    # Sprint 7 - Fases 75-76: Fills de drums
    @staticmethod
    def generate_fill(fill_type: str = "end_bar", energy: float = 0.7, 
                       bar_position: float = 0.0) -> List[NoteEvent]:
        """
        Genera fill de percusiÃ³n segÃºn tipo y energÃ­a.
        
        Args:
            fill_type: Tipo de fill - "end_bar", "crescendo", "transition"
            energy: Nivel de energÃ­a 0.0-1.0
            bar_position: PosiciÃ³n en beats donde inicia el fill
        
        Returns:
            Lista de NoteEvent para el fill
        """
        notes = []
        
        if fill_type == "end_bar":
            # 4 notas rÃ¡pidas en beats 3-4
            start_beat = bar_position + 2.0  # Beat 3
            pattern = [0, 0.5, 1.0, 1.5]  # Cada medio beat
            velocity_start = 80 + int(energy * 20)
            
            for i, offset in enumerate(pattern):
                vel = min(127, velocity_start + i * 10)
                # Alternar entre snare y tom
                pitch = 38 if i % 2 == 0 else 47
                notes.append(NoteEvent(pitch, start_beat + offset, 0.15, vel))
                
        elif fill_type == "crescendo":
            # Roll creciente
            num_notes = int(8 + energy * 8)  # 8-16 notas segÃºn energÃ­a
            duration_total = 2.0  # 2 beats
            note_duration = duration_total / num_notes
            
            for i in range(num_notes):
                start = bar_position + (i * note_duration)
                # Velocity creciente
                vel = int(60 + (i / num_notes) * 60)
                pitch = 38 if i % 2 == 0 else 42  # Alternar snare/hat
                notes.append(NoteEvent(pitch, start, note_duration * 0.8, min(127, vel)))
                
        elif fill_type == "transition":
            # Fill para transiciÃ³n segÃºn Î” energÃ­a
            # Asume que viene de una secciÃ³n de menor energÃ­a a mayor
            build_notes = int(4 + energy * 4)
            
            # Build con toms
            for i in range(build_notes):
                start = bar_position + (i * 0.25)
                vel = int(70 + i * 8)
                pitch = 45 + (i % 3) * 2  # Rotar entre toms
                notes.append(NoteEvent(pitch, start, 0.2, min(127, vel)))
            
            # Crash al final
            notes.append(NoteEvent(
                PercussionLibrary.FX_NOTES["crash"],
                bar_position + (build_notes * 0.25), 1.0, 110
            ))
        
        return notes
    
    # Sprint 7 - Fase 76: Snare Rolls
    @staticmethod
    def generate_snare_roll(duration: float = 2.0, subdivision: float = 0.125,
                           velocity_start: int = 60, velocity_end: int = 120,
                           position: float = 0.0) -> List[NoteEvent]:
        """
        Genera snare roll con 16th notes creciendo en velocity.
        
        Args:
            duration: DuraciÃ³n del roll en beats (default 2)
            subdivision: Intervalo entre notas (default 0.125 = 16th notes)
            velocity_start: Velocity inicial
            velocity_end: Velocity final
            position: PosiciÃ³n de inicio en beats
        
        Returns:
            Lista de NoteEvent para el roll
        """
        notes = []
        num_notes = int(duration / subdivision)
        
        for i in range(num_notes):
            start = position + (i * subdivision)
            # InterpolaciÃ³n lineal de velocity
            velocity = int(velocity_start + (velocity_end - velocity_start) * (i / num_notes))
            notes.append(NoteEvent(
                DembowPatterns.SNARE_NOTE,
                start,
                subdivision * 0.9,  # Ligeramente mÃ¡s corto para articulaciÃ³n
                min(127, velocity)
            ))
        
        return notes
    
    # Sprint 7 - Fase 81: Vocal Chops / Stabs
    @staticmethod
    def get_vocal_chop_pattern(pattern_name: str = "8th_pulse", 
                                bars: int = 4, root_note: int = 60) -> List[NoteEvent]:
        """
        Genera patrones de vocal chops/stabs para reggaeton.
        
        Args:
            pattern_name: Tipo de patrÃ³n - "8th_pulse", "16th_rhythm", "stutter", "triplets"
            bars: NÃºmero de compases
            root_note: Nota MIDI raÃ­z para los stabs
        
        Returns:
            Lista de NoteEvent para los vocal chops
        """
        notes = []
        
        if pattern_name == "8th_pulse":
            # Pulso en corcheas
            for bar in range(bars):
                bar_offset = bar * 4.0
                for eighth in range(8):
                    start = bar_offset + (eighth * 0.5)
                    # Solo en algunos pulsos
                    if eighth in [0, 2, 3, 5, 6]:
                        vel = 110 if eighth in [0, 5] else 85
                        notes.append(NoteEvent(root_note, start, 0.2, vel))
                        
        elif pattern_name == "16th_rhythm":
            # Ritmo en semicorcheas estilo reggaeton
            for bar in range(bars):
                bar_offset = bar * 4.0
                # PatrÃ³n caracterÃ­stico: 1, e, &, a
                pattern = [0, 0.25, 0.75, 1.0, 1.5, 1.75, 2.5, 3.0, 3.25, 3.75]
                for i, offset in enumerate(pattern):
                    vel = 100 if i in [0, 4, 7] else 75
                    notes.append(NoteEvent(root_note, bar_offset + offset, 0.1, vel))
                    
        elif pattern_name == "stutter":
            # Efecto stutter (repeticiÃ³n rÃ¡pida)
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Stutter en el beat 2 y 4
                for beat in [1.0, 3.0]:
                    for i in range(4):  # 4 repeticiones rÃ¡pidas
                        start = bar_offset + beat + (i * 0.0625)
                        vel = 100 - i * 10
                        notes.append(NoteEvent(root_note, start, 0.05, vel))
                        
        elif pattern_name == "triplets":
            # Tresillos
            for bar in range(bars):
                bar_offset = bar * 4.0
                # Tresillos en cada beat
                for beat in range(4):
                    triplet_start = bar_offset + beat
                    for i in range(3):
                        start = triplet_start + (i * (1/3))
                        vel = 90 if i == 0 else 70
                        notes.append(NoteEvent(root_note, start, 0.25, vel))
        
        return notes
    
    @staticmethod
    def create_stabs_track(track_name: str = "Stabs", pattern: str = "8th_pulse",
                          bars: int = 16, key: str = "A") -> Dict[str, Any]:
        """
        Crea configuraciÃ³n completa para track de Stabs/Vocal Chops.
        
        Returns:
            Dict con nombre, notas, y metadata del track
        """
        # Mapeo de keys a notas raÃ­z
        key_map = {"C": 60, "C#": 61, "D": 62, "D#": 63, "E": 64, "F": 65,
                   "F#": 66, "G": 67, "G#": 68, "A": 69, "A#": 70, "B": 71}
        root_note = key_map.get(key.upper(), 69)  # Default A4
        
        notes = PercussionLibrary.get_vocal_chop_pattern(pattern, bars, root_note)
        
        return {
            "track_name": track_name,
            "pattern": pattern,
            "bars": bars,
            "key": key,
            "root_note": root_note,
            "notes": notes,
            "note_count": len(notes)
        }


# Funciones de conveniencia

def create_drum_pattern(style: str = "dembow", bars: int = 16, humanize: bool = True) -> Dict[str, List[NoteEvent]]:
    """
    Crea patrÃ³n completo de baterÃ­a.
    
    Retorna dict con: kick, snare, hihat
    """
    dembow = DembowPatterns()
    
    kicks = dembow.get_kick_pattern(bars, variation=style if style in ["standard", "double", "triple", "minimal"] else "standard")
    snares = dembow.get_snare_pattern(bars, variation="standard")
    hihats = dembow.get_hihat_pattern(bars, style="16th", swing=0.6)
    
    if humanize:
        humanizer = HumanFeel()
        kicks = humanizer.apply_all_humanization(kicks, 10, 8, 3)
        snares = humanizer.apply_all_humanization(snares, 15, 10, 5)
        hihats = humanizer.apply_all_humanization(hihats, 5, 5, 2)
    
    return {
        "kick": kicks,
        "snare": snares,
        "hihat": hihats
    }


def create_full_arrangement(bars_per_section: int = 16, key: str = "A") -> Dict[str, Any]:
    """
    Crea arreglo completo de reggaeton.
    
    Retorna estructura con: intro, verse, chorus, bridge, outro
    """
    arrangement = {}
    
    # ProgresiÃ³n
    prog = ChordProgressions.get_progression("vi-IV-I-V", key, bars_per_section)
    
    # Intro
    arrangement["intro"] = {
        "drums": create_drum_pattern("minimal", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F"], key, "sustained"),
        "chords": prog,
        "percussion": PercussionLibrary.get_intro_buildup(4)
    }
    
    # Verso
    arrangement["verse"] = {
        "drums": create_drum_pattern("standard", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "sub"),
        "chords": prog,
        "melody": MelodyGenerator.generate_melody(bars_per_section, "pentatonic_minor", 0.4, key)
    }
    
    # Coro
    arrangement["chorus"] = {
        "drums": create_drum_pattern("double", bars_per_section, True),
        "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "pluck"),
        "chords": prog,
        "melody": MelodyGenerator.generate_melody(bars_per_section, "minor", 0.6, key)
    }
    
    return arrangement


# Constantes Ãºtiles
NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"]
DRUM_NOTES = {
    "kick": 36,
    "snare": 38,
    "clap": 39,
    "rim": 37,
    "hihat_closed": 42,
    "hihat_open": 46,
    "hihat_pedal": 44,
    "crash": 49,
    "ride": 51,
    "tom1": 50,
    "tom2": 47,
    "tom3": 43,
}


def notes_to_dict_list(notes: List[NoteEvent]) -> List[Dict[str, Any]]:
    """Convierte lista de NoteEvent a lista de diccionarios"""
    return [
        {
            "pitch": n.pitch,
            "start_time": n.start_time,
            "duration": n.duration,
            "velocity": n.velocity
        }
        for n in notes
    ]


def dict_list_to_notes(dict_list: List[Dict[str, Any]]) -> List[NoteEvent]:
    """Convierte lista de diccionarios a lista de NoteEvent"""
    return [
        NoteEvent(
            d["pitch"],
            d["start_time"],
            d["duration"],
            d["velocity"]
        )
        for d in dict_list
    ]


def get_patterns(pattern_type: str, **kwargs) -> Any:
    """
    FunciÃ³n conveniencia para obtener patrones musicales.
    
    Args:
        pattern_type: Tipo de patrÃ³n ('drum', 'bass', 'chords', 'melody', 'percussion', 'arrangement')
        **kwargs: Argumentos especÃ­ficos para cada tipo de patrÃ³n
        
    Returns:
        PatrÃ³n solicitado del tipo especificado
        
    Examples:
        >>> get_patterns('drum', style='dembow', bars=16)
        >>> get_patterns('bass', progression=['Am', 'F', 'C', 'G'], key='A', style='sub')
        >>> get_patterns('chords', progression_type='vi-IV-I-V', key='A', bars=16)
    """
    if pattern_type == "drum":
        return create_drum_pattern(**kwargs)
    elif pattern_type == "bass":
        return BassPatterns.get_bass_line(**kwargs)
    elif pattern_type == "chords":
        return ChordProgressions.get_progression(**kwargs)
    elif pattern_type == "melody":
        return MelodyGenerator.generate_melody(**kwargs)
    elif pattern_type == "percussion":
        return PercussionLibrary.get_percussion_fill(**kwargs)
    elif pattern_type == "arrangement":
        return create_full_arrangement(**kwargs)
    else:
        raise ValueError(f"Tipo de patrÃ³n no soportado: {pattern_type}")
