"""
song_generator.py - Generador musical para AbletonMCP-AI.
"""

import time
import random
import logging

from typing import List, Dict, Any, Optional, Union, Tuple

from dataclasses import dataclass

from pathlib import Path

from collections import defaultdict

try:
    from human_feel import HumanFeelEngine
except ImportError:
    HumanFeelEngine = None

logger = logging.getLogger("SongGenerator")



# Notas MIDI para referencia

NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']



# Escalas comunes (semitonos desde la raÃ­z)

SCALES = {

    'major': [0, 2, 4, 5, 7, 9, 11],

    'minor': [0, 2, 3, 5, 7, 8, 10],

    'harmonic_minor': [0, 2, 3, 5, 7, 8, 11],

    'dorian': [0, 2, 3, 5, 7, 9, 10],

    'phrygian': [0, 1, 3, 5, 7, 8, 10],

    'mixolydian': [0, 2, 4, 5, 7, 9, 10],

    'pentatonic_minor': [0, 3, 5, 7, 10],

    'pentatonic_major': [0, 2, 4, 7, 9],

    'blues': [0, 3, 5, 6, 7, 10],

}



# Progresiones de acordes comunes

CHORD_PROGRESSIONS = {

    'techno': [

        [1, 1, 1, 1],  # i - i - i - i (minimal)

        [1, 6, 1, 6],  # i - VI - i - VI

        [1, 4, 1, 4],  # i - iv - i - iv

        [1, 7, 6, 7],  # i - VII - VI - VII

    ],

    'house': [

        [1, 5, 6, 4],  # I - V - vi - IV (pop house)

        [1, 4, 5, 1],  # I - IV - V - I

        [6, 4, 1, 5],  # vi - IV - I - V

        [1, 6, 4, 5],  # I - vi - IV - V

    ],

    'deep': [

        [1, 6, 2, 5],  # i - VI - ii - V

        [2, 5, 1, 6],  # ii - V - i - VI

    ],

    'trance': [

        [1, 5, 6, 4],  # I - V - vi - IV

        [6, 4, 1, 5],  # vi - IV - I - V

        [1, 4, 6, 5],  # I - IV - vi - V

    ],
    '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
    },

}



# Configuraciones por gÃ©nero

GENRE_CONFIGS = {

    'techno': {

        'bpm_range': (125, 140),

        'default_bpm': 132,

        'keys': ['Am', 'Fm', 'Dm', 'G#m', 'Cm'],

        'styles': ['industrial', 'peak-time', 'dub', 'minimal', 'acid'],

    },

    'house': {

        'bpm_range': (120, 128),

        'default_bpm': 124,

        'keys': ['Am', 'Em', 'Cm', 'Gm', 'Dm', 'F#m'],

        'styles': ['deep', 'tech-house', 'progressive', 'afro', 'classic', 'funky'],

    },

    'tech-house': {

        'bpm_range': (122, 128),

        'default_bpm': 125,

        'keys': ['Am', 'Fm', 'Dm', 'Gm', 'Cm'],

        'styles': ['groovy', 'bouncy', 'minimal', 'latin', 'latin-industrial'],

    },

    'trance': {

        'bpm_range': (135, 150),

        'default_bpm': 140,

        'keys': ['Fm', 'Am', 'Dm', 'Gm', 'Cm'],

        'styles': ['progressive', 'uplifting', 'psy', 'acid'],

    },

    'drum-and-bass': {

        'bpm_range': (160, 180),

        'default_bpm': 174,

        'keys': ['Am', 'Fm', 'Gm', 'Cm'],

        'styles': ['liquid', 'neuro', 'jump-up', 'jungle'],

    },

    'reggaeton': {

        'bpm_range': (88, 98),

        'default_bpm': 92,

        'keys': ['Dm', 'Am', 'Fm', 'Gm', 'Cm'],

        'styles': ['dembow', 'perreo', 'latin', 'romantico'],

    },

}



# Colores por tipo de track

TRACK_COLORS = {

    'kick': 10,      # Rojo

    'snare': 20,     # Verde

    'hat': 5,        # Amarillo

    'clap': 45,      # Naranja

    'bass': 30,      # Azul

    'synth': 50,     # Rosa/Magenta

    'chords': 60,    # PÃºrpura

    'fx': 25,        # Verde claro

    'vocal': 15,     # Naranja oscuro

    'pad': 55,       # Purpura claro

    'perc': 20,      # Verde

    'ride': 14,      # Amarillo oscuro

    'technical': 58, # Gris

}



BUS_TRACK_COLORS = {

    'drums': 10,

    'bass': 30,

    'music': 50,

    'vocal': 15,

    'fx': 25,

    'sc_trigger': 58,  # Gris - track fantasma para sidechain

}



# Configuracion de sidechain por bus

# Cada bus puede tener sidechain desde SC TRIGGER

BUS_SIDECHAIN_CONFIG = {

    'drums': {

        'enabled': False,  # Drums no suele necesitar sidechain

        'threshold': -18.0,

        'attack': 0.003,

        'release': 0.08,

        'ratio': 4.0,

    },

    'bass': {

        'enabled': True,  # Sidechain clave para bass

        'threshold': -22.0,

        'attack': 0.002,

        'release': 0.12,

        'ratio': 4.5,

    },

    'music': {

        'enabled': True,  # Sidechain sutil para musica

        'threshold': -26.0,

        'attack': 0.005,

        'release': 0.18,

        'ratio': 3.0,

    },

    'vocal': {

        'enabled': True,  # Sidechain suave para vocal

        'threshold': -28.0,

        'attack': 0.008,

        'release': 0.22,

        'ratio': 2.5,

    },

    'fx': {

        'enabled': False,  # FX generalmente sin sidechain

        'threshold': -30.0,

        'attack': 0.01,

        'release': 0.3,

        'ratio': 2.0,

    },

}



# =============================================================================

# FASE 3: LOUDNESS CONSISTENCY Y GAIN STAGING

# =============================================================================

#

# CALIBRATION PHILOSOPHY:

# ======================

# - Kick sits at unity (0.85) as the rhythmic anchor

# - Bass sits slightly below kick (-1dB) for low-end presence without mud

# - Supporting elements progressively lower to create mix depth

# - Buses attenuated to preserve master headroom

# - Master chain with soft limiting for consistent output

#

# HEADROOM TARGETS:

# =================

# - Track peaks: -6dB to -3dB before bus

# - Bus peaks: -3dB to -1dB before master

# - Master out: -1dB peak (limited), integrated LUFS ~-10 to -8



# Headroom target en dB (negativo para dejar espacio antes del limiter)

TARGET_HEADROOM_DB = -1.5  # 1.5dB de headroom antes del limiter



# Safe limiting threshold - prevents digital clipping

MASTER_LIMITER_CEILING_DB = -0.3  # Never go above -0.3dBFS on master



# Calibracion de ganancia por bus (valores lineales 0.0-1.0)

# Calibrado empiricamente para headroom consistente y balance de mezcla

# K: Drums como elemento principal, B: Bass como soporte, M: Music como capa

BUS_GAIN_CALIBRATION = {

    'drums': {

        'volume': 0.92,           # Drums bus: principal, mas alto

        'limiter_gain': 0.0,      # Sin gain adicional en limiter de bus

        'compressor_threshold': -16.0,  # Compression suave para punch

        'saturator_drive': 0.6,   # armonia sutil, no crunchy

        'utility_gain': 0.0,      # Sin gain adicional

    },

    'bass': {

        'volume': 0.88,           # Bass bus: soporte fuerte

        'limiter_gain': 0.0,      # Sin limiter en bass bus (soft clip natural)

        'compressor_threshold': -18.0,  # Threshold suave para low-end

        'saturator_drive': 0.4,   # Saturacion sutil - evitar crunch

        'utility_gain': 0.0,      # Sin gain adicional

    },

    'music': {

        'volume': 0.85,           # Music bus: capa principal

        'limiter_gain': 0.0,      # Sin limiter en music bus

        'compressor_threshold': -20.0,  # Preservar transients

        'saturator_drive': 0.0,   # Sin saturacion en bus de musica

        'utility_gain': 0.0,

    },

    'vocal': {

        'volume': 0.82,           # Vocal bus: presente en mezcla

        'limiter_gain': 0.0,      # Sin limiter

        'compressor_threshold': -16.0,  # Compresion sutil para presencia

        'saturator_drive': 0.0,

        'utility_gain': 0.0,

    },

    'fx': {

        'volume': 0.78,           # FX bus: efectos audibles

        'limiter_gain': 0.0,      # Sin gain

        'compressor_threshold': -22.0,  # Preservar dynamics

        'saturator_drive': 0.0,

        'utility_gain': 0.0,      # Sin reduccion

    },

    'sc_trigger': {

        'volume': 0.0,            # Track fantasma - sin audio

        'limiter_gain': 0.0,

        'compressor_threshold': 0.0,

        'saturator_drive': 0.0,

        'utility_gain': 0.0,

    },

}



# Master chain calibracion

# Calibrado para LUFS ~-8 a -10dB con headroom de 1-2dB antes del limiter

# El limiter ceiling esta en -0.3dB para evitar digital clipping

MASTER_CALIBRATION = {

    'default': {

        'volume': 0.85,           # Master at ~0dB de ganancia interna

        'utility_gain': 0.0,      # Sin reduccion - volumen completo

        'stereo_width': 1.04,     # Ligerisimo widening

        'saturator_drive': 0.12,  # Saturacion muy sutil en master

        'compressor_ratio': 0.50, # Compresion suave (glue, no squash)

        'compressor_attack': 0.30, # Attack lento para preservar transients

        'compressor_release': 0.20,

        'limiter_gain': 3.5,      # +3.5dB make-up gain para nivel moderno

        'limiter_ceiling': -0.3,  # Ceiling a -0.3dBFS (safe limiting)

    },

    'warehouse': {

        'volume': 0.85,

        'utility_gain': 0.0,      # Sin reduccion

        'saturator_drive': 0.25,  # Mas drive para industrial techno

        'compressor_ratio': 0.55, # Un poco mas de compresion

        'limiter_gain': 3.8,      # Mas gain para industrial

        'limiter_ceiling': -0.3,

    },

    'festival': {

        'volume': 0.86,

        'utility_gain': 0.0,      # Sin reduccion

        'stereo_width': 1.06,     # Mas ancho para festival

        'limiter_gain': 4.0,      # Maximo gain para festival

        'limiter_ceiling': -0.3,

    },

    'swing': {

        'volume': 0.85,

        'utility_gain': 0.0,

        'saturator_drive': 0.15,  # Moderado

        'limiter_gain': 3.2,

        'limiter_ceiling': -0.3,

    },

    'jackin': {

        'volume': 0.85,

        'utility_gain': 0.0,

        'compressor_ratio': 0.52,

        'limiter_gain': 3.0,

        'limiter_ceiling': -0.3,

    },

    'tech-house-club': {

        'volume': 0.85,

        'utility_gain': 0.0,      # Sin reduccion

        'stereo_width': 1.04,

        'saturator_drive': 0.4,   # Mas drive para punch

        'compressor_ratio': 0.60,  # Mas compresion para club

        'compressor_attack': 0.28,

        'limiter_gain': 3.5,

        'limiter_ceiling': -0.3,

    },

    'tech-house-deep': {

        'volume': 0.85,

        'utility_gain': 0.0,      # Sin reduccion

        'stereo_width': 1.02,     # Narrower para deep

        'saturator_drive': 0.1,    # Muy sutil

        'compressor_ratio': 0.50,

        'compressor_attack': 0.38, # Mas lento para deep

        'limiter_gain': 3.0,

        'limiter_ceiling': -0.3,

    },

    'tech-house-funky': {

        'volume': 0.85,

        'utility_gain': 0.0,

        'stereo_width': 1.08,     # Wide para groove

        'saturator_drive': 0.3,

        'compressor_ratio': 0.55,

        'compressor_attack': 0.30,

        'limiter_gain': 3.5,

        'limiter_ceiling': -0.3,

    },

}



# Calibracion de gain por rol para consistencia de mezcla

# Valores calibrados empiricamente basados en:

# - Kick como ancla a 0.85

# - Bass -1dB relativo a kick

# - Elementos de soporte progresivamente mas bajos

# - Headroom preservado en cada capa

ROLE_GAIN_CALIBRATION = {

    # DRUMS - Kick es el ancla, otros elementos debajo

    'kick': {

        'volume': 0.85,           # Ancla: 0dB relativo, elemento principal

        'saturator_drive': 1.5,   # Saturacion sutil para punch

        'peak_reduction': 0.0,    # Sin reduccion - es el ancla

    },

    'clap': {

        'volume': 0.78,           # -1.5dB relativo a kick

        'saturator_drive': 0.0,   # Sin saturacion

        'peak_reduction': 0.0,

    },

    'snare_fill': {

        'volume': 0.72,           # -3dB, transitorio fuerte

        'peak_reduction': 0.0,

    },

    'hat_closed': {

        'volume': 0.68,           # -4dB, elemento secundario

        'peak_reduction': 0.0,

    },

    'hat_open': {

        'volume': 0.65,           # -4.5dB, mas abajo por sustain

        'peak_reduction': 0.0,

    },

    'top_loop': {

        'volume': 0.62,           # -5dB, capa ritmica secundaria

        'peak_reduction': 0.0,

    },

    'perc': {

        'volume': 0.70,           # -3.5dB, soporte ritmico

        'peak_reduction': 0.0,

    },

    'ride': {

        'volume': 0.58,           # -5.5dB, sustain largo

        'peak_reduction': 0.0,

    },

    'crash': {

        'volume': 0.50,           # -7dB, transitorio largo

        'peak_reduction': 0.0,

    },

    'tom_fill': {

        'volume': 0.68,           # -4dB, transitorio

        'peak_reduction': 0.0,

    },

    # BASS - Underground but underneath drums

    'sub_bass': {

        'volume': 0.80,           # -0.5dB relativo a kick

        'saturator_drive': 0.0,   # Sin saturacion en sub

        'peak_reduction': 0.0,

    },

    'bass': {

        'volume': 0.78,           # -1dB relativo a kick

        'saturator_drive': 2.0,   # Moderado para harmonic content

        'peak_reduction': 0.0,

    },

    # MUSIC - Capas de soporte, debajo del low-end

    'drone': {

        'volume': 0.55,           # -7dB, elemento de fondo

        'peak_reduction': 0.0,

    },

    'chords': {

        'volume': 0.70,           # -3dB, armonia principal

        'peak_reduction': 0.0,

    },

    'stab': {

        'volume': 0.65,           # -4dB, transitorio

        'saturator_drive': 1.8,   # Moderado

        'peak_reduction': 0.0,

    },

    'pad': {

        'volume': 0.60,           # -5dB, fondo armonico

        'peak_reduction': 0.0,

    },

    'pluck': {

        'volume': 0.68,           # -3.5dB, melodia sutil

        'peak_reduction': 0.0,

    },

    'arp': {

        'volume': 0.65,           # -4dB, movimiento armonico

        'peak_reduction': 0.0,

    },

    'lead': {

        'volume': 0.72,           # -2.5dB, elemento principal musical

        'saturator_drive': 1.2,   # Moderado

        'peak_reduction': 0.0,

    },

    'counter': {

        'volume': 0.62,           # -5dB, contramelodia

        'peak_reduction': 0.0,

    },

    # FX - Efectos en el fondo de la mezcla

    'reverse_fx': {

        'volume': 0.52,           # -7dB, efecto ambiente

        'peak_reduction': 0.0,

    },

    'riser': {

        'volume': 0.60,           # -5dB, sube hacia el climax

        'peak_reduction': 0.0,

    },

    'impact': {

        'volume': 0.55,           # -6dB, efecto puntual

        'peak_reduction': 0.0,

    },

    'atmos': {

        'volume': 0.50,           # -8dB, fondo atmosferico

        'peak_reduction': 0.0,

    },

    # VOCAL

    'vocal': {

        'volume': 0.70,           # -3dB, debajo de drums pero presente

        'peak_reduction': 0.0,

    },

    # SC TRIGGER - Track fantasma para sidechain

    'sc_trigger': {

        'volume': 0.0,            # Sin salida de audio

        'saturator_drive': 0.0,

        'peak_reduction': 0.0,

    },

}



# Factores de ajuste por estilo

# NOTA: NO usar multiplicadores de volumen que rompan el gain staging

# Solo ajustes sutiles de procesamiento y sends

STYLE_GAIN_ADJUSTMENTS = {

    'industrial': {

        'saturator_drive_factor': 1.3,     # Aumentar drive en elementos agresivos

        'additional_heat_send': 0.05,      # Un poco mas de heat

        'limiter_gain_factor': 1.15,       # +15% gain para industrial techno

    },

    'latin': {

        'additional_pan_width': 0.05,

    },

    'peak-time': {

        'master_compressor_ratio_factor': 1.1,

        'limiter_gain_factor': 1.1,        # +10% gain para peak-time

    },

    'minimal': {

        'fx_bus_send_reduction': 0.05,

        'additional_space_send': 0.03,     # Un poco mas de reverb para espacio

    },

}



ROLE_BUS_ASSIGNMENTS = {

    'sc_trigger': 'sc_trigger',  # Rutea a su propio bus fantasma

    'kick': 'drums',

    'clap': 'drums',

    'snare_fill': 'drums',

    'hat_closed': 'drums',

    'hat_open': 'drums',

    'top_loop': 'drums',

    'perc': 'drums',

    'tom_fill': 'drums',

    'ride': 'drums',

    'crash': 'drums',

    'sub_bass': 'bass',

    'bass': 'bass',

    'drone': 'music',

    'chords': 'music',

    'stab': 'music',

    'pad': 'music',

    'pluck': 'music',

    'arp': 'music',

    'lead': 'music',

    'counter': 'music',

    'reverse_fx': 'fx',

    'riser': 'fx',

    'impact': 'fx',

    'atmos': 'fx',

    'vocal': 'vocal',

}



SECTION_BLUEPRINTS = {

    'minimal': [

        ('INTRO', 8, 12, 'intro', 1),

        ('GROOVE', 16, 20, 'build', 2),

        ('BREAK', 8, 25, 'break', 1),

        ('OUTRO', 8, 8, 'outro', 1),

    ],

    'standard': [

        ('INTRO', 8, 12, 'intro', 1),

        ('BUILD', 8, 18, 'build', 2),

        ('DROP A', 16, 28, 'drop', 4),

        ('BREAK', 8, 25, 'break', 1),

        ('DROP B', 16, 30, 'drop', 5),

        ('OUTRO', 8, 8, 'outro', 1),

    ],

    'extended': [

        ('INTRO DJ', 16, 10, 'intro', 1),

        ('BUILD A', 8, 18, 'build', 2),

        ('DROP A', 16, 28, 'drop', 4),

        ('BREAKDOWN', 8, 25, 'break', 1),

        ('BUILD B', 8, 18, 'build', 3),

        ('DROP B', 16, 30, 'drop', 5),

        ('OUTRO DJ', 16, 8, 'outro', 1),

    ],

    'club': [

        ('INTRO DJ', 16, 10, 'intro', 1),

        ('GROOVE A', 16, 14, 'build', 2),

        ('VOCAL BUILD', 8, 18, 'build', 3),

        ('DROP A', 16, 28, 'drop', 4),

        ('BREAKDOWN', 8, 25, 'break', 1),

        ('BUILD B', 8, 18, 'build', 3),

        ('DROP B', 16, 30, 'drop', 5),

        ('PEAK', 8, 32, 'drop', 5),

        ('OUTRO DJ', 16, 8, 'outro', 1),

    ],

    # P1 Sprint v0.1.23: Reggaeton/perreo-specific structures
    'reggaeton': [
        ('INTRO', 8, 12, 'intro', 1),
        ('GROOVE A', 16, 16, 'build', 2),
        ('DROP A', 16, 28, 'drop', 4),
        ('CORO', 8, 22, 'break', 1),
        ('GROOVE B', 8, 18, 'build', 3),
        ('DROP B', 16, 30, 'drop', 5),
        ('OUTRO', 8, 8, 'outro', 1),
    ],

    'perreo_duro': [
        ('INTRO DJ', 16, 10, 'intro', 1),
        ('DEM BOW A', 16, 14, 'build', 2),
        ('PERREO A', 16, 28, 'drop', 4),
        ('PUENTE', 8, 24, 'break', 1),
        ('DEM BOW B', 8, 18, 'build', 3),
        ('PERREO B', 16, 31, 'drop', 5),
        ('OUTRO DJ', 16, 8, 'outro', 1),
    ],

    'safaera_style': [
        ('INTRO', 4, 12, 'intro', 1),
        ('BUILD A', 8, 16, 'build', 2),
        ('DROP A', 16, 28, 'drop', 4),
        ('MID BREAK', 4, 20, 'break', 1),
        ('DROP B', 16, 30, 'drop', 5),
        ('PUENTE', 8, 22, 'break', 2),
        ('DROP C', 16, 32, 'drop', 6),
        ('OUTRO', 8, 10, 'outro', 1),
    ],

}



SECTION_BLUEPRINT_VARIANTS = {

    'standard': [

        SECTION_BLUEPRINTS['standard'],

        [

            ('INTRO', 8, 12, 'intro', 1),

            ('GROOVE A', 8, 16, 'build', 2),

            ('DROP A', 16, 28, 'drop', 4),

            ('BREAKDOWN', 8, 24, 'break', 1),

            ('BUILD B', 8, 20, 'build', 3),

            ('DROP B', 16, 31, 'drop', 5),

        ],

        [

            ('INTRO DJ', 16, 10, 'intro', 1),

            ('BUILD', 8, 18, 'build', 2),

            ('DROP A', 16, 28, 'drop', 4),

            ('MID BREAK', 8, 22, 'break', 1),

            ('PEAK', 16, 31, 'drop', 5),

        ],

    ],

    'club': [

        SECTION_BLUEPRINTS['club'],

        [

            ('INTRO DJ', 16, 10, 'intro', 1),

            ('TEASE', 8, 14, 'build', 2),

            ('GROOVE A', 16, 18, 'build', 3),

            ('DROP A', 16, 28, 'drop', 4),

            ('BREAKDOWN', 8, 24, 'break', 1),

            ('BUILD B', 8, 20, 'build', 3),

            ('PEAK', 16, 32, 'drop', 5),

            ('OUTRO DJ', 24, 8, 'outro', 1),

        ],

        [

            ('INTRO DJ', 16, 10, 'intro', 1),

            ('GROOVE A', 16, 15, 'build', 2),

            ('VOCAL BUILD', 8, 20, 'build', 3),

            ('DROP A', 16, 27, 'drop', 4),

            ('MID BREAK', 8, 22, 'break', 1),

            ('GROOVE B', 8, 18, 'build', 3),

('DROP B', 24, 31, 'drop', 5),
            ('OUTRO DJ', 16, 8, 'outro', 1),
        ],
    ],

    'reggaeton': [
        SECTION_BLUEPRINTS['reggaeton'],
        [
            ('INTRO', 8, 10, 'intro', 1),
            ('GROOVE', 16, 14, 'build', 2),
            ('DROP A', 16, 26, 'drop', 4),
            ('CORO', 8, 22, 'break', 1),
            ('DROP B', 16, 30, 'drop', 5),
            ('OUTRO', 8, 10, 'outro', 1),
        ],
        [
            ('INTRO DJ', 16, 10, 'intro', 1),
            ('PERREO A', 16, 16, 'build', 2),
            ('DROP A', 16, 28, 'drop', 4),
            ('PUENTE', 8, 22, 'break', 1),
            ('BUILD B', 8, 18, 'build', 3),
            ('DROP B', 16, 30, 'drop', 5),
            ('OUTRO', 12, 10, 'outro', 1),
        ],
    ],

    'perreo_duro': [
        SECTION_BLUEPRINTS['perreo_duro'],
        [
            ('INTRO', 8, 12, 'intro', 1),
            ('DEM BOW', 16, 14, 'build', 2),
            ('PERREO', 16, 28, 'drop', 4),
            ('CORO', 8, 24, 'break', 1),
            ('PERREO B', 16, 30, 'drop', 5),
            ('OUTRO', 8, 10, 'outro', 1),
        ],
        [
            ('INTRO DJ', 12, 10, 'intro', 1),
            ('BUILD A', 8, 16, 'build', 2),
            ('DROP A', 20, 28, 'drop', 4),
            ('MID BREAK', 8, 20, 'break', 1),
            ('DROP B', 16, 30, 'drop', 5),
            ('OUTRO DJ', 16, 10, 'outro', 1),
        ],
    ],

    'safaera_style': [
        SECTION_BLUEPRINTS['safaera_style'],
        [
            ('INTRO', 4, 14, 'intro', 1),
            ('DEM BOW', 12, 16, 'build', 2),
            ('DROP A', 16, 30, 'drop', 4),
            ('PUENTE', 4, 22, 'break', 1),
            ('DROP B', 16, 31, 'drop', 5),
            ('OUTRO', 8, 10, 'outro', 1),
        ],
        [
            ('INTRO DJ', 16, 10, 'intro', 1),
            ('BUILD', 8, 18, 'build', 2),
            ('PERREO A', 20, 28, 'drop', 4),
            ('CORO', 8, 24, 'break', 1),
            ('PERREO B', 16, 30, 'drop', 5),
            ('PUENTE', 4, 20, 'break', 2),
            ('DROP C', 16, 32, 'drop', 6),
            ('OUTRO DJ', 12, 10, 'outro', 1),
        ],
    ],

}



ROLE_ACTIVITY = {

    'sc_trigger': {'intro': 4, 'build': 4, 'drop': 4, 'break': 2, 'outro': 3},

    'kick': {'intro': 2, 'build': 3, 'drop': 4, 'break': 1, 'outro': 2},

    'clap': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},

    'snare_fill': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},

    'hat_closed': {'intro': 1, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},

    'hat_open': {'intro': 0, 'build': 1, 'drop': 3, 'break': 0, 'outro': 1},

    'top_loop': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},

    'perc': {'intro': 0, 'build': 2, 'drop': 3, 'break': 1, 'outro': 0},

    'tom_fill': {'intro': 0, 'build': 1, 'drop': 1, 'break': 0, 'outro': 0},

    'ride': {'intro': 0, 'build': 1, 'drop': 2, 'break': 0, 'outro': 1},

    'crash': {'intro': 0, 'build': 1, 'drop': 1, 'break': 0, 'outro': 0},

    'sub_bass': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},

    'bass': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},

    'drone': {'intro': 2, 'build': 2, 'drop': 2, 'break': 3, 'outro': 2},

    'chords': {'intro': 0, 'build': 2, 'drop': 3, 'break': 2, 'outro': 1},

    'stab': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 0},

    'pad': {'intro': 2, 'build': 2, 'drop': 2, 'break': 3, 'outro': 2},

    'pluck': {'intro': 0, 'build': 2, 'drop': 3, 'break': 0, 'outro': 0},

    'arp': {'intro': 0, 'build': 2, 'drop': 3, 'break': 1, 'outro': 0},

    'lead': {'intro': 0, 'build': 1, 'drop': 4, 'break': 0, 'outro': 0},

    'counter': {'intro': 0, 'build': 1, 'drop': 3, 'break': 1, 'outro': 0},

    'reverse_fx': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},

    'riser': {'intro': 0, 'build': 3, 'drop': 1, 'break': 2, 'outro': 0},

    'impact': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},

    'atmos': {'intro': 2, 'build': 1, 'drop': 1, 'break': 3, 'outro': 2},

    'vocal': {'intro': 0, 'build': 1, 'drop': 2, 'break': 1, 'outro': 0},

}



# ROLE_MIX: Perfil de mezcla por rol

# Valores base que luego se calibran con ROLE_GAIN_CALIBRATION

# Volumenes calibrados relativos: kick = 0%, otros debajo

# Pan y sends optimizados para profundidad y espacio

ROLE_MIX = {

    'sc_trigger': {'volume': 0.0, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.0}},

    # DRUMS - Kick centered, elements below

    'kick': {'volume': 0.85, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.08}},

    'clap': {'volume': 0.78, 'pan': 0.0, 'sends': {'space': 0.14, 'echo': 0.04, 'heat': 0.02, 'glue': 0.10}},

    'snare_fill': {'volume': 0.72, 'pan': 0.0, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.01, 'glue': 0.06}},

    'hat_closed': {'volume': 0.68, 'pan': -0.10, 'sends': {'space': 0.04, 'echo': 0.03, 'heat': 0.0, 'glue': 0.04}},

    'hat_open': {'volume': 0.65, 'pan': 0.12, 'sends': {'space': 0.10, 'echo': 0.08, 'heat': 0.01, 'glue': 0.06}},

    'top_loop': {'volume': 0.62, 'pan': -0.16, 'sends': {'space': 0.06, 'echo': 0.12, 'heat': 0.0, 'glue': 0.08}},

    'perc': {'volume': 0.70, 'pan': 0.20, 'sends': {'space': 0.10, 'echo': 0.14, 'heat': 0.02, 'glue': 0.10}},

    'tom_fill': {'volume': 0.68, 'pan': 0.12, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.01, 'glue': 0.06}},

    'ride': {'volume': 0.58, 'pan': 0.24, 'sends': {'space': 0.04, 'echo': 0.03, 'heat': 0.0, 'glue': 0.06}},

    'crash': {'volume': 0.50, 'pan': 0.0, 'sends': {'space': 0.18, 'echo': 0.06, 'heat': 0.01, 'glue': 0.02}},

    # BASS - Below drums, centered for mono compatibility

    'sub_bass': {'volume': 0.80, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.14}},

    'bass': {'volume': 0.78, 'pan': 0.0, 'sends': {'space': 0.01, 'echo': 0.01, 'heat': 0.04, 'glue': 0.12}},

    # MUSIC - Layers below rhythm section

    'drone': {'volume': 0.55, 'pan': 0.0, 'sends': {'space': 0.28, 'echo': 0.08, 'heat': 0.02, 'glue': 0.04}},

    'chords': {'volume': 0.70, 'pan': -0.06, 'sends': {'space': 0.18, 'echo': 0.12, 'heat': 0.01, 'glue': 0.08}},

    'stab': {'volume': 0.65, 'pan': 0.10, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.04, 'glue': 0.08}},

    'pad': {'volume': 0.60, 'pan': -0.14, 'sends': {'space': 0.32, 'echo': 0.08, 'heat': 0.0, 'glue': 0.06}},

    'pluck': {'volume': 0.68, 'pan': 0.14, 'sends': {'space': 0.08, 'echo': 0.18, 'heat': 0.01, 'glue': 0.06}},

    'arp': {'volume': 0.65, 'pan': -0.18, 'sends': {'space': 0.14, 'echo': 0.24, 'heat': 0.01, 'glue': 0.08}},

    'lead': {'volume': 0.72, 'pan': 0.06, 'sends': {'space': 0.14, 'echo': 0.18, 'heat': 0.03, 'glue': 0.10}},

    'counter': {'volume': 0.62, 'pan': 0.20, 'sends': {'space': 0.18, 'echo': 0.14, 'heat': 0.01, 'glue': 0.06}},

    # FX - Deep in the mix

    'reverse_fx': {'volume': 0.52, 'pan': 0.0, 'sends': {'space': 0.24, 'echo': 0.10, 'heat': 0.03, 'glue': 0.02}},

    'riser': {'volume': 0.60, 'pan': 0.0, 'sends': {'space': 0.28, 'echo': 0.14, 'heat': 0.04, 'glue': 0.03}},

    'impact': {'volume': 0.55, 'pan': 0.0, 'sends': {'space': 0.22, 'echo': 0.12, 'heat': 0.01, 'glue': 0.03}},

    'atmos': {'volume': 0.50, 'pan': -0.20, 'sends': {'space': 0.34, 'echo': 0.06, 'heat': 0.0, 'glue': 0.03}},

    # VOCAL - Present but under drums

    'vocal': {'volume': 0.70, 'pan': 0.08, 'sends': {'space': 0.20, 'echo': 0.24, 'heat': 0.02, 'glue': 0.10}},

}



ARRANGEMENT_PROFILES = (

    {

        'name': 'warehouse',

        'genres': {'techno', 'tech-house'},

        'drum_tightness': 1.15,

        'bass_motion': 'locked',

        'melodic_motion': 'restrained',

        'pan_width': 0.12,

        'fx_bias': 1.0,

    },

    {

        'name': 'jackin',

        'genres': {'house', 'tech-house'},

        'drum_tightness': 0.96,

        'bass_motion': 'bouncy',

        'melodic_motion': 'call_response',

        'pan_width': 0.16,

        'fx_bias': 0.92,

    },

    {

        'name': 'festival',

        'genres': {'trance', 'house', 'tech-house'},

        'drum_tightness': 0.92,

        'bass_motion': 'lifted',

        'melodic_motion': 'anthemic',

        'pan_width': 0.2,

        'fx_bias': 1.18,

    },

    {

        'name': 'swing',

        'genres': {'tech-house', 'house'},

        'drum_tightness': 0.9,

        'bass_motion': 'syncopated',

        'melodic_motion': 'hooky',

        'pan_width': 0.22,

        'fx_bias': 1.05,

    },

    {

        'name': 'tech-house-club',

        'genres': {'tech-house'},

        'drum_tightness': 0.94,

        'bass_motion': 'bouncy',

        'melodic_motion': 'hooky',

        'pan_width': 0.18,

        'fx_bias': 1.08,

        'bus_names': {

            'drums': 'DRUM CLUB',

            'bass': 'BASS TUBE',

            'music': 'MUSIC JACK',

            'vocal': 'VOCAL LATIN BUS',

            'fx': 'FX JAM',

        },

        'return_names': {

            'space': 'REVERB SHORT',

            'echo': 'DELAY MONO',

            'heat': 'DRIVE HOT',

            'glue': 'GLUE BUS',

        },

    },

    {

        'name': 'tech-house-deep',

        'genres': {'tech-house'},

        'drum_tightness': 1.02,

        'bass_motion': 'locked',

        'melodic_motion': 'restrained',

        'pan_width': 0.14,

        'fx_bias': 0.88,

        'bus_names': {

            'drums': 'DRUM DEEP',

            'bass': 'SUB DEEP',

            'music': 'ATMOS DEEP',

            'vocal': 'VOX DEEP',

            'fx': 'FX DEEP',

        },

        'return_names': {

            'space': 'REVERB DEEP',

            'echo': 'DELAY DEEP',

            'heat': 'SATURATE DEEP',

            'glue': 'GLUE MINIMAL',

        },

    },

    {

        'name': 'tech-house-funky',

        'genres': {'tech-house'},

        'drum_tightness': 0.86,

        'bass_motion': 'syncopated',

        'melodic_motion': 'hooky',

        'pan_width': 0.24,

        'fx_bias': 1.12,

        'bus_names': {

            'drums': 'DRUM GROOVE',

            'bass': 'BASS FUNK',

            'music': 'MUSIC GROOVE',

            'vocal': 'VOCAL FUNK',

            'fx': 'FX SWING',

        },

        'return_names': {

            'space': 'REVERB GROOVE',

            'echo': 'DELAY GROOVE',

            'heat': 'DRIVE FUNK',

            'glue': 'GLUE SWING',

        },

    },

    {

        'name': 'reggaeton-dembow',

        'genres': {'reggaeton'},

        'drum_tightness': 0.94,

        'bass_motion': 'syncopated',

        'melodic_motion': 'hooky',

        'pan_width': 0.18,

        'fx_bias': 0.98,

        'bus_names': {

            'drums': 'DRUM DEMBOW',

            'bass': 'BASS URBANO',

            'music': 'MUSIC LATIN',

            'vocal': 'VOCAL URBANO',

            'fx': 'FX DEMBOW',

        },

        'return_names': {

            'space': 'ROOM LATIN',

            'echo': 'DELAY LATIN',

            'heat': 'DRIVE URBANO',

            'glue': 'GLUE LATIN',

        },

    },

)



ROLE_FX_CHAINS = {

    'sc_trigger': [

        {'device': 'Utility', 'parameters': {'Gain': 0.0, 'Width': 0.0}},

    ],

    'kick': [

        {'device': 'Saturator', 'parameters': {'Drive': 2.5}},

    ],

    'clap': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.08}},

    ],

    'snare_fill': [

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}},

    ],

    'hat_closed': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 15000.0, 'Dry/Wet': 0.14}},

    ],

    'hat_open': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 12000.0, 'Dry/Wet': 0.18}},

    ],

    'top_loop': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 11000.0, 'Dry/Wet': 0.22}},

    ],

    'perc': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 9500.0, 'Dry/Wet': 0.16}},

    ],

    'ride': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 12500.0, 'Dry/Wet': 0.12}},

    ],

    'sub_bass': [

        {'device': 'Utility', 'parameters': {'Width': 0.0}},

    ],

    'bass': [

        {'device': 'Saturator', 'parameters': {'Drive': 4.0}},

        {'device': 'Auto Filter', 'parameters': {'Frequency': 7800.0, 'Dry/Wet': 0.12}},

    ],

    'drone': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.16}},

    ],

    'chords': [

        {'device': 'Auto Filter', 'parameters': {'Frequency': 9800.0, 'Dry/Wet': 0.14}},

    ],

    'stab': [

        {'device': 'Saturator', 'parameters': {'Drive': 3.0}},

    ],

    'pad': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.18}},

    ],

    'pluck': [

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.12}},

    ],

    'arp': [

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.16}},

    ],

    'lead': [

        {'device': 'Saturator', 'parameters': {'Drive': 2.0}},

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.12}},

    ],

    'counter': [

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.1}},

    ],

    'crash': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.16}},

    ],

    'reverse_fx': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.24}},

    ],

    'riser': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.28}},

    ],

    'impact': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}},

    ],

    'atmos': [

        {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.3}},

    ],

    'vocal': [

        {'device': 'Echo', 'parameters': {'Dry/Wet': 0.14}},

    ],

}



SCRIPTS_ROOT = Path(__file__).resolve().parents[2]

REFERENCE_SEARCH_DIRS = (

    SCRIPTS_ROOT / 'sample',

    SCRIPTS_ROOT / 'samples',

)

REFERENCE_TRACK_PROFILES = [

    {

        'name': 'Eli Brown x GeezLy - Me Gusta',

        'match_terms': ['eli brown', 'geezly', 'me gusta'],

        'genre': 'tech-house',

        'style': 'latin-industrial',

        'bpm': 136.0,

        'key': 'F#m',

        'structure': 'club',

        'reference_bars': 112,

    },

    {

        'name': 'Mr. Pauer, Goyo - QuÃ­mica',

        'match_terms': ['mr. pauer', 'goyo', 'quÃ­mica'],

        'genre': 'house',

        'style': 'latin-funky vocal',

        'bpm': 123.0,

        'key': 'Cm',

        'structure': 'extended',

        'reference_bars': 72,

    },

]



# =========================================================================

# SECTION AUTOMATION PARAMETERS

# =========================================================================



SECTION_AUTOMATION = {

    'intro': {

        'energy': 0.25,

        'filters': {

            'drums': {'frequency': 8500.0, 'resonance': 0.3, 'dry_wet': 0.12},

            'bass': {'frequency': 6200.0, 'resonance': 0.25, 'dry_wet': 0.08},

            'music': {'frequency': 7800.0, 'resonance': 0.2, 'dry_wet': 0.1},

            'vocal': {'frequency': 9200.0, 'resonance': 0.15, 'dry_wet': 0.06},

            'fx': {'frequency': 8800.0, 'resonance': 0.18, 'dry_wet': 0.14},

        },

        'reverb': {'send_level': 0.28, 'decay_time': 2.8, 'size': 0.85},

        'delay': {'send_level': 0.18, 'feedback': 0.35, 'time_l': 0.375, 'time_r': 0.5},

        'compression': {'threshold': -14.0, 'ratio': 2.0, 'attack': 0.015, 'release': 0.12},

        'saturation': {'drive': 0.8, 'mix': 0.15},

        'stereo_width': {'value': 0.92},

        'envelope_curve': 'ease_in',

    },

    'build': {

        'energy': 0.72,

        'filters': {

            'drums': {'frequency': 4200.0, 'resonance': 0.45, 'dry_wet': 0.22},

            'bass': {'frequency': 3800.0, 'resonance': 0.35, 'dry_wet': 0.16},

            'music': {'frequency': 5400.0, 'resonance': 0.28, 'dry_wet': 0.18},

            'vocal': {'frequency': 6800.0, 'resonance': 0.22, 'dry_wet': 0.12},

            'fx': {'frequency': 5200.0, 'resonance': 0.32, 'dry_wet': 0.24},

        },

        'reverb': {'send_level': 0.18, 'decay_time': 2.2, 'size': 0.72},

        'delay': {'send_level': 0.32, 'feedback': 0.48, 'time_l': 0.375, 'time_r': 0.5},

        'compression': {'threshold': -10.0, 'ratio': 3.5, 'attack': 0.008, 'release': 0.08},

        'saturation': {'drive': 2.2, 'mix': 0.28},

        'stereo_width': {'value': 1.08},

        'envelope_curve': 'ramp_up',

    },

    'drop': {

        'energy': 1.0,

        'filters': {

            'drums': {'frequency': 14500.0, 'resonance': 0.2, 'dry_wet': 0.04},

            'bass': {'frequency': 9800.0, 'resonance': 0.15, 'dry_wet': 0.03},

            'music': {'frequency': 12200.0, 'resonance': 0.12, 'dry_wet': 0.05},

            'vocal': {'frequency': 12800.0, 'resonance': 0.1, 'dry_wet': 0.04},

            'fx': {'frequency': 11000.0, 'resonance': 0.15, 'dry_wet': 0.08},

        },

        'reverb': {'send_level': 0.12, 'decay_time': 1.6, 'size': 0.55},

        'delay': {'send_level': 0.14, 'feedback': 0.28, 'time_l': 0.25, 'time_r': 0.375},

        'compression': {'threshold': -6.0, 'ratio': 4.5, 'attack': 0.005, 'release': 0.06},

        'saturation': {'drive': 3.5, 'mix': 0.38},

        'stereo_width': {'value': 1.18},

        'envelope_curve': 'punch',

    },

    'break': {

        'energy': 0.38,

        'filters': {

            'drums': {'frequency': 5200.0, 'resonance': 0.55, 'dry_wet': 0.32},

            'bass': {'frequency': 2800.0, 'resonance': 0.45, 'dry_wet': 0.24},

            'music': {'frequency': 6400.0, 'resonance': 0.35, 'dry_wet': 0.22},

            'vocal': {'frequency': 8200.0, 'resonance': 0.28, 'dry_wet': 0.16},

            'fx': {'frequency': 6800.0, 'resonance': 0.38, 'dry_wet': 0.28},

        },

        'reverb': {'send_level': 0.42, 'decay_time': 3.5, 'size': 1.0},

        'delay': {'send_level': 0.38, 'feedback': 0.52, 'time_l': 0.5, 'time_r': 0.75},

        'compression': {'threshold': -18.0, 'ratio': 1.8, 'attack': 0.025, 'release': 0.18},

        'saturation': {'drive': 0.5, 'mix': 0.1},

        'stereo_width': {'value': 1.25},

        'envelope_curve': 'ease_out',

    },

    'outro': {

        'energy': 0.32,

        'filters': {

            'drums': {'frequency': 6200.0, 'resonance': 0.35, 'dry_wet': 0.18},

            'bass': {'frequency': 4200.0, 'resonance': 0.28, 'dry_wet': 0.14},

            'music': {'frequency': 5600.0, 'resonance': 0.25, 'dry_wet': 0.16},

            'vocal': {'frequency': 7200.0, 'resonance': 0.2, 'dry_wet': 0.1},

            'fx': {'frequency': 6400.0, 'resonance': 0.28, 'dry_wet': 0.2},

        },

        'reverb': {'send_level': 0.35, 'decay_time': 3.2, 'size': 0.92},

        'delay': {'send_level': 0.28, 'feedback': 0.42, 'time_l': 0.375, 'time_r': 0.5},

        'compression': {'threshold': -12.0, 'ratio': 2.2, 'attack': 0.018, 'release': 0.15},

        'saturation': {'drive': 0.6, 'mix': 0.12},

        'stereo_width': {'value': 0.98},

        'envelope_curve': 'ease_out',

    },

}



# Envelope curve templates for automation interpolation

ENVELOPE_CURVES = {

    'linear': lambda x: x,

    'ease_in': lambda x: x * x,

    'ease_out': lambda x: 1 - (1 - x) ** 2,

    'ease_in_out': lambda x: 3 * x * x - 2 * x * x * x,

    'ramp_up': lambda x: x ** 0.5,

    'ramp_down': lambda x: 1 - (1 - x) ** 2,

    'punch': lambda x: min(1.0, x * 2.0) if x < 0.5 else 1.0 - (1.0 - x) ** 0.5,

    's_curve': lambda x: 1 / (1 + (2.71828 ** (-10 * (x - 0.5)))),

    'exponential': lambda x: (2.71828 ** (x - 1) - 0.3679) / 0.6321,

}



# =============================================================================

# AUTOMATIZACION DE DEVICES POR SECCION - FASE 2

# Parametros especificos por device para cada tipo de seccion

# =============================================================================



# Automatizacion de devices en tracks individuales por rol - ENHANCED

SECTION_DEVICE_AUTOMATION = {

    # BASS - Filtros, drive y compresion dinamica

    'bass': {

        'Saturator': {

            'Drive': {'intro': 1.5, 'build': 3.5, 'drop': 5.0, 'break': 2.0, 'outro': 1.8},

            'Dry/Wet': {'intro': 0.12, 'build': 0.22, 'drop': 0.30, 'break': 0.15, 'outro': 0.10},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6200.0, 'build': 8500.0, 'drop': 12000.0, 'break': 4800.0, 'outro': 5800.0},

            'Dry/Wet': {'intro': 0.08, 'build': 0.18, 'drop': 0.12, 'break': 0.22, 'outro': 0.06},

            'Resonance': {'intro': 0.25, 'build': 0.35, 'drop': 0.20, 'break': 0.40, 'outro': 0.28},

        },

        'Compressor': {

            'Threshold': {'intro': -12.0, 'build': -14.0, 'drop': -18.0, 'break': -10.0, 'outro': -11.0},

            'Ratio': {'intro': 2.5, 'build': 3.0, 'drop': 4.0, 'break': 2.0, 'outro': 2.2},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},

        },

    },

    'sub_bass': {

        'Saturator': {

            'Drive': {'intro': 1.0, 'build': 2.5, 'drop': 4.0, 'break': 1.5, 'outro': 1.2},

        },

        'Auto Filter': {

            'Frequency': {'intro': 5200.0, 'build': 7200.0, 'drop': 10000.0, 'break': 4200.0, 'outro': 4800.0},

            'Dry/Wet': {'intro': 0.05, 'build': 0.10, 'drop': 0.06, 'break': 0.14, 'outro': 0.04},

        },

        'Utility': {

            'Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},

            'Gain': {'intro': 0.0, 'build': 0.2, 'drop': 0.4, 'break': -0.2, 'outro': 0.0},

        },

    },

    # PAD - Filtros envolventes con width y reverb

    'pad': {

        'Auto Filter': {

            'Frequency': {'intro': 4500.0, 'build': 8000.0, 'drop': 11000.0, 'break': 3200.0, 'outro': 4000.0},

            'Dry/Wet': {'intro': 0.25, 'build': 0.18, 'drop': 0.12, 'break': 0.35, 'outro': 0.28},

            'Resonance': {'intro': 0.20, 'build': 0.28, 'drop': 0.15, 'break': 0.35, 'outro': 0.22},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.22, 'build': 0.16, 'drop': 0.10, 'break': 0.28, 'outro': 0.24},

            'Decay Time': {'intro': 3.5, 'build': 2.8, 'drop': 2.0, 'break': 4.2, 'outro': 3.8},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.85, 'build': 1.02, 'drop': 1.12, 'break': 1.25, 'outro': 0.90},

        },

        'Saturator': {

            'Drive': {'intro': 0.8, 'build': 1.5, 'drop': 2.5, 'break': 0.6, 'outro': 0.7},

            'Dry/Wet': {'intro': 0.10, 'build': 0.15, 'drop': 0.20, 'break': 0.08, 'outro': 0.12},

        },

    },

    # ATMOS - Filtros espaciales con movement

    'atmos': {

        'Auto Filter': {

            'Frequency': {'intro': 3800.0, 'build': 7200.0, 'drop': 9800.0, 'break': 2800.0, 'outro': 3500.0},

            'Dry/Wet': {'intro': 0.30, 'build': 0.22, 'drop': 0.15, 'break': 0.40, 'outro': 0.32},

            'Resonance': {'intro': 0.22, 'build': 0.32, 'drop': 0.18, 'break': 0.42, 'outro': 0.25},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.35, 'build': 0.28, 'drop': 0.18, 'break': 0.42, 'outro': 0.38},

            'Decay Time': {'intro': 4.0, 'build': 3.2, 'drop': 2.2, 'break': 5.0, 'outro': 4.5},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.70, 'build': 0.88, 'drop': 1.05, 'break': 1.20, 'outro': 0.75},

        },

    },

    # FX ELEMENTS

    'reverse_fx': {

        'Auto Filter': {

            'Frequency': {'intro': 5200.0, 'build': 9000.0, 'drop': 12000.0, 'break': 6000.0, 'outro': 4800.0},

            'Dry/Wet': {'intro': 0.20, 'build': 0.28, 'drop': 0.15, 'break': 0.35, 'outro': 0.22},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.30, 'build': 0.35, 'drop': 0.20, 'break': 0.40, 'outro': 0.28},

            'Decay Time': {'intro': 3.0, 'build': 4.5, 'drop': 2.5, 'break': 5.5, 'outro': 3.5},

        },

        'Saturator': {

            'Drive': {'intro': 1.2, 'build': 2.8, 'drop': 4.5, 'break': 1.8, 'outro': 1.0},

        },

    },

    'riser': {

        'Auto Filter': {

            'Frequency': {'intro': 4000.0, 'build': 10000.0, 'drop': 14000.0, 'break': 5500.0, 'outro': 4200.0},

            'Dry/Wet': {'intro': 0.15, 'build': 0.30, 'drop': 0.12, 'break': 0.22, 'outro': 0.18},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.25, 'build': 0.40, 'drop': 0.22, 'break': 0.35, 'outro': 0.20},

            'Decay Time': {'intro': 2.5, 'build': 5.0, 'drop': 3.0, 'break': 4.0, 'outro': 2.8},

        },

        'Echo': {

            'Dry/Wet': {'intro': 0.18, 'build': 0.35, 'drop': 0.15, 'break': 0.25, 'outro': 0.15},

            'Feedback': {'intro': 0.30, 'build': 0.55, 'drop': 0.25, 'break': 0.45, 'outro': 0.28},

        },

        'Saturator': {

            'Drive': {'intro': 1.5, 'build': 4.0, 'drop': 3.0, 'break': 2.5, 'outro': 1.2},

        },

    },

    'impact': {

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.15, 'build': 0.18, 'drop': 0.12, 'break': 0.20, 'outro': 0.14},

            'Decay Time': {'intro': 2.0, 'build': 2.5, 'drop': 1.8, 'break': 3.0, 'outro': 2.2},

        },

        'Saturator': {

            'Drive': {'intro': 1.8, 'build': 2.5, 'drop': 3.5, 'break': 2.0, 'outro': 1.5},

        },

    },

    'drone': {

        'Auto Filter': {

            'Frequency': {'intro': 3000.0, 'build': 6500.0, 'drop': 9000.0, 'break': 2500.0, 'outro': 2800.0},

            'Dry/Wet': {'intro': 0.20, 'build': 0.15, 'drop': 0.10, 'break': 0.30, 'outro': 0.22},

            'Resonance': {'intro': 0.25, 'build': 0.35, 'drop': 0.22, 'break': 0.40, 'outro': 0.28},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.18, 'build': 0.14, 'drop': 0.08, 'break': 0.25, 'outro': 0.20},

            'Decay Time': {'intro': 4.5, 'build': 3.5, 'drop': 2.5, 'break': 5.5, 'outro': 4.8},

        },

        'Saturator': {

            'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.8, 'break': 0.6, 'outro': 0.7},

        },

    },

    # HATS - Filtros de brillantez con resonance y saturacion

    'hat_closed': {

        'Auto Filter': {

            'Frequency': {'intro': 12000.0, 'build': 14000.0, 'drop': 16000.0, 'break': 10000.0, 'outro': 11000.0},

            'Dry/Wet': {'intro': 0.12, 'build': 0.16, 'drop': 0.10, 'break': 0.20, 'outro': 0.14},

            'Resonance': {'intro': 0.15, 'build': 0.25, 'drop': 0.12, 'outro': 0.18, 'break': 0.30},

        },

        'Saturator': {

            'Drive': {'intro': 0.5, 'build': 1.2, 'drop': 1.8, 'break': 0.8, 'outro': 0.6},

        },

    },

    'hat_open': {

        'Auto Filter': {

            'Frequency': {'intro': 9000.0, 'build': 11000.0, 'drop': 13000.0, 'break': 7500.0, 'outro': 8500.0},

            'Dry/Wet': {'intro': 0.18, 'build': 0.22, 'drop': 0.14, 'break': 0.28, 'outro': 0.20},

            'Resonance': {'intro': 0.18, 'build': 0.28, 'drop': 0.15, 'outro': 0.20, 'break': 0.35},

        },

        'Echo': {

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.22, 'outro': 0.12},

        },

    },

    'top_loop': {

        'Auto Filter': {

            'Frequency': {'intro': 8500.0, 'build': 10500.0, 'drop': 12500.0, 'break': 7000.0, 'outro': 8000.0},

            'Dry/Wet': {'intro': 0.20, 'build': 0.25, 'drop': 0.16, 'break': 0.32, 'outro': 0.22},

            'Resonance': {'intro': 0.12, 'build': 0.22, 'drop': 0.14, 'outro': 0.15, 'break': 0.28},

        },

        'Echo': {

            'Dry/Wet': {'intro': 0.05, 'build': 0.12, 'drop': 0.08, 'break': 0.18, 'outro': 0.10},

        },

    },

    # SYNTHS

    'chords': {

        'Auto Filter': {

            'Frequency': {'intro': 5500.0, 'build': 8500.0, 'drop': 11000.0, 'break': 4000.0, 'outro': 5000.0},

            'Dry/Wet': {'intro': 0.15, 'build': 0.20, 'drop': 0.12, 'break': 0.28, 'outro': 0.18},

            'Resonance': {'intro': 0.18, 'build': 0.28, 'drop': 0.15, 'outro': 0.20, 'break': 0.35},

        },

        'Echo': {

            'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.08, 'break': 0.22, 'outro': 0.12},

            'Feedback': {'intro': 0.25, 'build': 0.40, 'drop': 0.30, 'break': 0.45, 'outro': 0.28},

        },

        'Saturator': {

            'Drive': {'intro': 1.2, 'build': 2.2, 'drop': 3.5, 'break': 1.5, 'outro': 1.0},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.95, 'build': 1.05, 'drop': 1.15, 'break': 1.25, 'outro': 1.00},

        },

    },

    'lead': {

        'Saturator': {

            'Drive': {'intro': 1.0, 'build': 2.5, 'drop': 4.0, 'break': 1.5, 'outro': 1.2},

            'Dry/Wet': {'intro': 0.12, 'build': 0.20, 'drop': 0.25, 'break': 0.10, 'outro': 0.15},

        },

        'Echo': {

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.18, 'outro': 0.10},

            'Feedback': {'intro': 0.20, 'build': 0.35, 'drop': 0.28, 'break': 0.40, 'outro': 0.22},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6500.0, 'build': 9500.0, 'drop': 12500.0, 'break': 4500.0, 'outro': 5500.0},

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.90, 'build': 1.02, 'drop': 1.10, 'break': 1.18, 'outro': 0.95},

        },

    },

    'stab': {

        'Saturator': {

            'Drive': {'intro': 2.0, 'build': 3.5, 'drop': 5.0, 'break': 2.5, 'outro': 2.2},

            'Dry/Wet': {'intro': 0.18, 'build': 0.25, 'drop': 0.30, 'break': 0.15, 'outro': 0.20},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6000.0, 'build': 9000.0, 'drop': 12000.0, 'break': 5000.0, 'outro': 5500.0},

            'Dry/Wet': {'intro': 0.10, 'build': 0.15, 'drop': 0.08, 'break': 0.22, 'outro': 0.12},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.88, 'build': 1.00, 'drop': 1.12, 'break': 1.20, 'outro': 0.92},

        },

    },

    'pluck': {

        'Echo': {

            'Dry/Wet': {'intro': 0.12, 'build': 0.22, 'drop': 0.14, 'break': 0.28, 'outro': 0.15},

            'Feedback': {'intro': 0.30, 'build': 0.45, 'drop': 0.35, 'break': 0.50, 'outro': 0.32},

        },

        'Auto Filter': {

            'Frequency': {'intro': 7000.0, 'build': 10000.0, 'drop': 13000.0, 'break': 5500.0, 'outro': 6500.0},

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},

        },

        'Saturator': {

            'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.8, 'break': 1.2, 'outro': 0.9},

        },

    },

    'arp': {

        'Echo': {

            'Dry/Wet': {'intro': 0.15, 'build': 0.28, 'drop': 0.18, 'break': 0.35, 'outro': 0.18},

            'Feedback': {'intro': 0.35, 'build': 0.50, 'drop': 0.40, 'break': 0.58, 'outro': 0.38},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6500.0, 'build': 9500.0, 'drop': 12500.0, 'break': 5000.0, 'outro': 6000.0},

            'Dry/Wet': {'intro': 0.12, 'build': 0.18, 'drop': 0.14, 'break': 0.25, 'outro': 0.15},

        },

        'Saturator': {

            'Drive': {'intro': 0.6, 'build': 1.5, 'drop': 2.5, 'break': 1.0, 'outro': 0.7},

        },

    },

    'counter': {

        'Echo': {

            'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.12, 'break': 0.22, 'outro': 0.12},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6000.0, 'build': 8800.0, 'drop': 11500.0, 'break': 4800.0, 'outro': 5200.0},

            'Dry/Wet': {'intro': 0.10, 'build': 0.16, 'drop': 0.12, 'break': 0.22, 'outro': 0.14},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.75, 'build': 0.92, 'drop': 1.08, 'break': 1.15, 'outro': 0.80},

        },

    },

    # VOCAL

    'vocal': {

        'Echo': {

            'Dry/Wet': {'intro': 0.12, 'build': 0.25, 'drop': 0.15, 'break': 0.30, 'outro': 0.14},

            'Feedback': {'intro': 0.25, 'build': 0.42, 'drop': 0.30, 'break': 0.48, 'outro': 0.28},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.06, 'break': 0.18, 'outro': 0.10},

            'Decay Time': {'intro': 2.5, 'build': 3.5, 'drop': 2.0, 'break': 4.0, 'outro': 2.8},

        },

        'Auto Filter': {

            'Frequency': {'intro': 6000.0, 'build': 9000.0, 'drop': 11000.0, 'break': 5000.0, 'outro': 5500.0},

            'Dry/Wet': {'intro': 0.10, 'build': 0.16, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},

        },

        'Saturator': {

            'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.5, 'break': 1.2, 'outro': 0.9},

        },

    },

    # DRUMS - Sin automatizacion de devices (manejados por volumen/sends)

    'kick': {},

    'clap': {},

    'snare_fill': {},

    'perc': {},

    'ride': {},

    'tom_fill': {},

    'crash': {},

    'sc_trigger': {},

}



# Automatizacion de devices en BUSES por seccion - ENHANCED

BUS_DEVICE_AUTOMATION = {

    'drums': {

        'Compressor': {

            'Threshold': {'intro': -14.0, 'build': -16.0, 'drop': -18.5, 'break': -12.0, 'outro': -13.5},

            'Ratio': {'intro': 2.5, 'build': 3.0, 'drop': 4.0, 'break': 2.2, 'outro': 2.4},

            'Attack': {'intro': 0.015, 'build': 0.010, 'drop': 0.005, 'break': 0.020, 'outro': 0.018},

        },

        'Saturator': {

            'Drive': {'intro': 0.8, 'build': 1.5, 'drop': 2.5, 'break': 1.0, 'outro': 0.9},

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.22, 'break': 0.10, 'outro': 0.10},

        },

        'Limiter': {

            'Gain': {'intro': 0.2, 'build': 0.3, 'drop': 0.5, 'break': 0.15, 'outro': 0.18},

        },

        'AutoFilter': {

            'Frequency': {'intro': 8500.0, 'build': 12500.0, 'drop': 16000.0, 'break': 4500.0, 'outro': 6500.0},

            'Dry/Wet': {'intro': 0.10, 'build': 0.22, 'drop': 0.04, 'break': 0.35, 'outro': 0.18},

            'Resonance': {'intro': 0.20, 'build': 0.12, 'drop': 0.08, 'break': 0.50, 'outro': 0.28},

        },

    },

    'bass': {

        'Saturator': {

            'Drive': {'intro': 1.0, 'build': 2.0, 'drop': 3.5, 'break': 1.5, 'outro': 1.2},

            'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.25, 'break': 0.12, 'outro': 0.10},

        },

        'Compressor': {

            'Threshold': {'intro': -15.0, 'build': -17.0, 'drop': -20.0, 'break': -14.0, 'outro': -14.5},

            'Ratio': {'intro': 3.0, 'build': 3.5, 'drop': 4.5, 'break': 2.8, 'outro': 3.0},

            'Attack': {'intro': 0.020, 'build': 0.015, 'drop': 0.008, 'break': 0.025, 'outro': 0.022},

        },

        'Utility': {

            'Stereo Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},

        },

        'Auto Filter': {

            'Frequency': {'intro': 4800.0, 'build': 8500.0, 'drop': 12000.0, 'break': 3200.0, 'outro': 4200.0},

            'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.05, 'break': 0.25, 'outro': 0.12},

            'Resonance': {'intro': 0.18, 'build': 0.12, 'drop': 0.08, 'break': 0.45, 'outro': 0.22},

        },

    },

    'music': {

        'Compressor': {

            'Threshold': {'intro': -19.0, 'build': -20.0, 'drop': -22.0, 'break': -18.0, 'outro': -18.5},

            'Ratio': {'intro': 2.0, 'build': 2.5, 'drop': 3.0, 'break': 1.8, 'outro': 2.0},

            'Attack': {'intro': 0.025, 'build': 0.020, 'drop': 0.015, 'break': 0.030, 'outro': 0.028},

        },

        'Auto Filter': {

            'Frequency': {'intro': 7500.0, 'build': 12000.0, 'drop': 16000.0, 'break': 4500.0, 'outro': 6000.0},

            'Dry/Wet': {'intro': 0.12, 'build': 0.18, 'drop': 0.03, 'break': 0.30, 'outro': 0.15},

            'Resonance': {'intro': 0.18, 'build': 0.10, 'drop': 0.06, 'break': 0.40, 'outro': 0.22},

        },

        'Utility': {

            'Stereo Width': {'intro': 1.02, 'build': 1.08, 'drop': 1.12, 'break': 1.25, 'outro': 1.05},

        },

        'Saturator': {

            'Drive': {'intro': 0.3, 'build': 0.8, 'drop': 1.5, 'break': 0.4, 'outro': 0.35},

            'Dry/Wet': {'intro': 0.05, 'build': 0.10, 'drop': 0.15, 'break': 0.08, 'outro': 0.06},

        },

    },

    'vocal': {

        'Echo': {

            'Dry/Wet': {'intro': 0.06, 'build': 0.12, 'drop': 0.05, 'break': 0.18, 'outro': 0.08},

            'Feedback': {'intro': 0.25, 'build': 0.42, 'drop': 0.28, 'break': 0.50, 'outro': 0.30},

        },

        'Compressor': {

            'Threshold': {'intro': -16.0, 'build': -17.0, 'drop': -19.0, 'break': -15.0, 'outro': -15.5},

            'Ratio': {'intro': 2.8, 'build': 3.2, 'drop': 3.8, 'break': 2.5, 'outro': 2.7},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.06, 'build': 0.10, 'drop': 0.03, 'break': 0.16, 'outro': 0.08},

            'Decay Time': {'intro': 2.2, 'build': 3.0, 'drop': 1.6, 'break': 4.0, 'outro': 2.5},

        },

        'Auto Filter': {

            'Frequency': {'intro': 8000.0, 'build': 11500.0, 'drop': 14500.0, 'break': 6000.0, 'outro': 7200.0},

            'Dry/Wet': {'intro': 0.08, 'build': 0.12, 'drop': 0.04, 'break': 0.22, 'outro': 0.10},

            'Resonance': {'intro': 0.15, 'build': 0.10, 'drop': 0.06, 'break': 0.32, 'outro': 0.18},

        },

    },

    'fx': {

        'Auto Filter': {

            'Frequency': {'intro': 6000.0, 'build': 10500.0, 'drop': 14000.0, 'break': 4000.0, 'outro': 5200.0},

            'Dry/Wet': {'intro': 0.15, 'build': 0.20, 'drop': 0.06, 'outro': 0.18, 'break': 0.35},

            'Resonance': {'intro': 0.18, 'build': 0.15, 'drop': 0.10, 'break': 0.42, 'outro': 0.22},

        },

        'Hybrid Reverb': {

            'Dry/Wet': {'intro': 0.20, 'build': 0.25, 'drop': 0.10, 'break': 0.38, 'outro': 0.22},

            'Decay Time': {'intro': 3.0, 'build': 3.8, 'drop': 2.0, 'break': 5.0, 'outro': 3.5},

        },

        'Limiter': {

            'Gain': {'intro': -0.3, 'build': 0.0, 'drop': 0.2, 'break': -0.5, 'outro': -0.2},

        },

        'Saturator': {

            'Drive': {'intro': 0.5, 'build': 1.5, 'drop': 2.2, 'break': 0.8, 'outro': 0.6},

            'Dry/Wet': {'intro': 0.08, 'build': 0.14, 'drop': 0.20, 'break': 0.10, 'outro': 0.10},

        },

    },

}



# Automatizacion de devices en MASTER por seccion - ENHANCED

MASTER_DEVICE_AUTOMATION = {

    'Utility': {'Stereo Width': {'intro': 1.04, 'build': 1.08, 'drop': 1.10, 'break': 1.12, 'outro': 1.06},

        'Gain': {'intro': 0.72, 'build': 0.88, 'drop': 1.0, 'break': 0.68, 'outro': 0.70},

    },

    'Saturator': {'Drive': {'intro': 0.18, 'build': 0.30, 'drop': 0.45, 'break': 0.12, 'outro': 0.15},

        'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.22, 'break': 0.06, 'outro': 0.10},

    },

    'Compressor': {'Ratio': {'intro': 0.55, 'build': 0.62, 'drop': 0.68, 'break': 0.50, 'outro': 0.52},

        'Threshold': {'intro': -10.0, 'build': -12.0, 'drop': -13.5, 'break': -8.0, 'outro': -9.0},

        'Attack': {'intro': 0.020, 'build': 0.015, 'drop': 0.010, 'break': 0.025, 'outro': 0.022},

        'Release': {'intro': 0.15, 'build': 0.12, 'drop': 0.10, 'break': 0.18, 'outro': 0.16},

    },

    'Limiter': {'Gain': {'intro': 1.05, 'build': 1.12, 'drop': 1.20, 'break': 1.00, 'outro': 1.02},

        'Ceiling': {'intro': -0.5, 'build': -0.7, 'drop': -0.9, 'break': -0.4, 'outro': -0.45},

    },

    'Auto Filter': {'Frequency': {'intro': 8500.0, 'build': 12000.0, 'drop': 16000.0, 'break': 5500.0, 'outro': 7500.0},

        'Dry/Wet': {'intro': 0.04, 'build': 0.02, 'drop': 0.01, 'break': 0.06, 'outro': 0.05},

    },

    'Echo': {'Dry/Wet': {'intro': 0.02, 'build': 0.05, 'drop': 0.03, 'break': 0.07, 'outro': 0.03},

        'Feedback': {'intro': 0.15, 'build': 0.25, 'drop': 0.18, 'break': 0.30, 'outro': 0.20},

    },

}



DEVICE_PARAMETER_SAFETY_CLAMPS = {

    'Drive': {'min': 0.0, 'max': 6.0},

    'Frequency': {'min': 20.0, 'max': 20000.0},

    'Dry/Wet': {'min': 0.0, 'max': 1.0},

    'Feedback': {'min': 0.0, 'max': 0.7},

    'Stereo Width': {'min': 0.0, 'max': 1.3},

    'Resonance': {'min': 0.0, 'max': 1.0},

    'Ratio': {'min': 1.0, 'max': 20.0},

    'Threshold': {'min': -60.0, 'max': 0.0},

    'Attack': {'min': 0.0001, 'max': 0.5},

    'Release': {'min': 0.001, 'max': 2.0},

    'Gain': {'min': -1.0, 'max': 1.8},

    'Decay Time': {'min': 0.1, 'max': 10.0},

}



MASTER_SAFETY_CLAMPS = {

    'Stereo Width': {'min': 0.0, 'max': 1.25},

    'Drive': {'min': 0.0, 'max': 1.5},

    'Ratio': {'min': 0.45, 'max': 0.9},

    'Gain': {'min': 0.0, 'max': 1.6},

    'Attack': {'min': 0.0001, 'max': 0.1},

    'Ceiling': {'min': -3.0, 'max': 0.0},

    'Threshold': {'min': -20.0, 'max': 0.0},

    'Release': {'min': 0.001, 'max': 1.0},

}



# Expanded configuration de variaciÃ³n por secciÃ³n

SECTION_VARIATION_CONFIG = {

    'perc': {

        'intro': {'sparse': True, 'intensity': 0.3, 'variant': 'ghost'},

        'build': {'building': True, 'intensity': 0.8, 'variant': 'layering'},

        'drop': {'full': True, 'intensity': 1.0, 'variant': 'layered'},

        'break': {'sparse': True, 'intensity': 0.4, 'variant': 'minimal'},

        'outro': {'fading': True, 'intensity': 0.3, 'variant': 'strip_down'},

    },

    'perc_alt': {

        'intro': {'sparse': True, 'intensity': 0.2, 'variant': 'minimal'},

        'build': {'building': True, 'intensity': 0.6, 'variant': 'tension'},

        'drop': {'full': True, 'intensity': 0.7, 'variant': 'groove'},

        'break': {'sparse': True, 'intensity': 0.3, 'variant': 'atmos'},

        'outro': {'fading': True, 'intensity': 0.2, 'variant': 'minimal'},

    },

    'top_loop': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'intensity': 0.8, 'variant': 'energy'},

        'drop': {'full': True, 'intensity': 1.0, 'variant': 'full'},

        'break': {'sparse': True, 'intensity': 0.4, 'variant': 'filtered'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'hat_open': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'intensity': 0.7, 'variant': 'tease'},

        'drop': {'full': True, 'intensity': 0.9, 'variant': 'offbeat'},

        'break': {'sparse': True, 'intensity': 0.3, 'variant': 'filtered'},

        'outro': {'fading': True, 'intensity': 0.4, 'variant': 'fading'},

    },

    'ride': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'intensity': 0.6, 'variant': 'building'},

        'drop': {'full': True, 'intensity': 0.8, 'variant': 'full'},

        'break': {'sparse': True, 'intensity': 0.3, 'variant': 'sparse'},

        'outro': {'fading': True, 'intensity': 0.4, 'variant': 'minimal'},

    },

    'snare_fill': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'tension': True, 'intensity': 0.8, 'variant': 'rolling'},

        'drop': {'impact': True, 'intensity': 0.6, 'variant': 'fill'},

        'break': {'sparse': True, 'intensity': 0.5, 'variant': 'tension'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'tom_fill': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'rising': True, 'intensity': 0.7, 'variant': 'rising'},

        'drop': {'impact': True, 'intensity': 0.5, 'variant': 'fill'},

        'break': {'use': False, 'variant': 'absent'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'vocal_shot': {

        'intro': {'sparse': True, 'variant': 'hint'},

        'build': {'building': True, 'variant': 'anticipate'},

        'drop': {'full': True, 'variant': 'hook'},

        'break': {'sparse': True, 'variant': 'filtered'},

        'outro': {'fading': True, 'variant': 'minimal'},

    },

    'synth_peak': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'variant': 'rising'},

        'drop': {'full': True, 'variant': 'anthem'},

        'break': {'use': False, 'variant': 'absent'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'atmos': {

        'intro': {'full': True, 'decay': 'long', 'variant': 'atmospheric'},

        'build': {'building': True, 'variant': 'tension'},

        'drop': {'sparse': True, 'variant': 'minimal'},

        'break': {'full': True, 'decay': 'long', 'variant': 'ethereal'},

        'outro': {'fading': True, 'decay': 'long', 'variant': 'fading'},

    },

    'chords': {

        'intro': {'sparse': True, 'variant': 'foreshadow'},

        'build': {'building': True, 'variant': 'rising'},

        'drop': {'full': True, 'variant': 'full'},

        'break': {'sparse': True, 'variant': 'atmospheric'},

        'outro': {'fading': True, 'variant': 'echo'},

    },

    'pad': {

        'intro': {'full': True, 'variant': 'atmospheric'},

        'build': {'building': True, 'variant': 'tension'},

        'drop': {'sparse': True, 'variant': 'minimal'},

        'break': {'full': True, 'variant': 'ethereal'},

        'outro': {'fading': True, 'variant': 'decay'},

    },

    'lead': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'variant': 'rising'},

        'drop': {'full': True, 'variant': 'hook'},

        'break': {'sparse': True, 'variant': 'minimal'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'arp': {

        'intro': {'sparse': True, 'variant': 'ghost'},

        'build': {'building': True, 'variant': 'energy'},

        'drop': {'full': True, 'variant': 'driving'},

        'break': {'sparse': True, 'variant': 'filtered'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'pluck': {

        'intro': {'sparse': True, 'variant': 'hint'},

        'build': {'building': True, 'variant': 'tension'},

        'drop': {'full': True, 'variant': 'punchy'},

        'break': {'sparse': True, 'variant': 'minimal'},

        'outro': {'fading': True, 'variant': 'strip_down'},

    },

    'bass': {

        'intro': {'sparse': True, 'variant': 'subtle'},

        'build': {'building': True, 'variant': 'rising'},

        'drop': {'full': True, 'variant': 'groove'},

        'break': {'sparse': True, 'variant': 'filtered'},

        'outro': {'fading': True, 'variant': 'fading'},

    },

    'sub_bass': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'building': True, 'variant': 'hint'},

        'drop': {'full': True, 'variant': 'deep'},

        'break': {'sparse': True, 'variant': 'minimal'},

        'outro': {'use': False, 'variant': 'absent'},

    },

    'stab': {

        'intro': {'use': False, 'variant': 'absent'},

        'build': {'sparse': True, 'variant': 'hint'},

        'drop': {'full': True, 'variant': 'impact'},

        'break': {'use': False, 'variant': 'absent'},

        'outro': {'use': False, 'variant': 'absent'},

    },

}



# =========================================================================

# PATTERN VARIATION SYSTEM - Anti-repetition tracking

# =========================================================================



class PatternVariationManager:

    """

    Manages pattern variant selection with cross-generation memory

    to prevent repetitive patterns across sections and generations.

    """



    def __init__(self):

        self.memory: Dict[str, Dict[str, int]] = {

            'drum': {},

            'bass': {},

            'melodic': {},

        }

        self.section_signatures: List[str] = []

        self.max_memory_age = 5  # Generations before decay



    def record_usage(self, category: str, variant: str) -> None:

        """Record that a pattern variant was used."""

        if category not in self.memory:

            self.memory[category] = {}

        self.memory[category][variant] = self.memory[category].get(variant, 0) + 1

        logger.debug(f"[PATTERN_MEMORY] Recorded {category}:{variant} (count: {self.memory[category][variant]})")



    def get_penalty(self, category: str, variant: str) -> float:

        """Get penalty score for a variant based on recent usage."""

        count = self.memory.get(category, {}).get(variant, 0)

        penalty = min(0.4, count * 0.08)  # Max 40% penalty

        if penalty > 0:

            logger.debug(f"[PATTERN_MEMORY] Penalty for {category}:{variant} = {penalty:.2f} (used {count}x)")

        return penalty



    def decay_memory(self) -> None:

        """Decay memory to allow reuse after generations."""

        for category in self.memory:

            for variant in list(self.memory[category].keys()):

                self.memory[category][variant] = max(0, self.memory[category][variant] - 1)

                if self.memory[category][variant] <= 0:

                    del self.memory[category][variant]



    def reset(self) -> None:

        """Reset all memory."""

        self.memory = {'drum': {}, 'bass': {}, 'melodic': {}}

        self.section_signatures = []

        logger.info("[PATTERN_MEMORY] Reset all pattern variant memory")



    def compute_section_signature(self, section: Dict[str, Any]) -> str:

        """Compute a signature for section to detect repetition."""

        drum_variants = section.get('drum_role_variants', {})

        signature_parts = [

            f"k:{drum_variants.get('kick', 'default')}",

            f"c:{drum_variants.get('clap', 'default')}",

            f"h:{drum_variants.get('hat_closed', 'default')}",

            f"b:{section.get('bass_bank_variant', 'anchor')}",

            f"m:{section.get('melodic_bank_variant', 'motif')}",

            f"d:{section.get('density', 1.0):.1f}",

        ]

        return "|".join(signature_parts)



    def check_repetition(self, sections: List[Dict[str, Any]]) -> List[Tuple[int, str]]:

        """Check for repetitive sections and return warnings."""

        warnings = []

        signatures = []

        consecutive_same = 0



        for i, section in enumerate(sections):

            sig = self.compute_section_signature(section)

            signatures.append(sig)



            if signatures and len(signatures) > 1 and signatures[-2] == sig:

                consecutive_same += 1

                if consecutive_same >= 2:

                    warning_msg = f"[REPETITION_DETECTED] Sections {i-1}-{i} have identical signature: {sig}"

                    logger.warning(warning_msg)

                    warnings.append((i, sig))

            else:

                consecutive_same = 0



        return warnings



# Global pattern variation manager

_pattern_variation_manager = PatternVariationManager()



def get_pattern_manager() -> PatternVariationManager:

    """Get the global pattern variation manager."""

    return _pattern_variation_manager



# Legacy compatibility functions

def _get_pattern_variant_penalty(category: str, variant: str) -> float:

    """Get penalty for a pattern variant (legacy wrapper)."""

    return _pattern_variation_manager.get_penalty(category, variant)



def _record_pattern_variant_usage(category: str, variant: str) -> None:

    """Record pattern variant usage (legacy wrapper)."""

    _pattern_variation_manager.record_usage(category, variant)



def _decay_pattern_variant_memory() -> None:

    """Decay pattern variant memory (legacy wrapper)."""

    _pattern_variation_manager.decay_memory()



def reset_pattern_variant_memory() -> None:

    """Reset all pattern variant memory (legacy wrapper)."""

    _pattern_variation_manager.reset()



# =============================================================================

# MUSICAL THEME SYSTEM - Shared motif/hook generator for unified track identity

# =============================================================================



class MusicalTheme:
    """
    Shared theme that evolves across sections to create unified musical identity.
    
    This class generates a base melodic motif (2-4 bar hook) and provides variations
    for different sections (intro, build, drop, break, outro). The theme ensures
    bass, chords, and lead are derived from the same musical idea, creating
    coherence across all melodic elements.
    
    Usage:
        theme = MusicalTheme(key='Am', scale='minor')
        
        # Get variations for different sections
        intro_motif = theme.get_section_variation('intro')
        drop_motif = theme.get_section_variation('drop')
        
        # Derive musical elements
        bass_notes = theme.motif_to_bass(drop_motif)
        chord_prog = theme.motif_to_chords(drop_motif)
        lead_notes = theme.motif_to_lead(drop_motif)
    """
    
    def __init__(self, key='Am', scale='minor', seed=None):
        self.key = key
        self.scale = scale
        self.seed = seed or random.randint(0, 10000)
        self.rng = random.Random(self.seed)
        self.base_motif = self._generate_base_motif()
        self.variations = {}
        
    def _get_scale_notes(self, octave=4):
        """Get scale notes for the theme's key and scale."""
        root_note = self.key[:-1] if len(self.key) > 1 else self.key
        base_octave = octave
        
        # Convert note name to MIDI
        note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
        root_note = root_note.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')
        root_note = root_note.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')
        
        try:
            note_idx = note_names.index(root_note.upper())
            root_midi = (base_octave + 1) * 12 + note_idx
        except ValueError:
            root_midi = 60  # Default C4
            
        # Get scale intervals
        scale_intervals = SCALES.get(self.scale, SCALES['minor'])
        return [root_midi + interval for interval in scale_intervals]
    
    def _generate_base_motif(self, bars=2):
        """
        Generate a 2-4 bar melodic hook based on the key and scale.
        
        Returns a list of note dicts with pitch, time, duration, velocity.
        """
        scale_notes = self._get_scale_notes(octave=4)
        motif = []
        
        # Generate 1-2 bar motif, then optionally repeat for 2-4 bars
        motif_bars = self.rng.choice([1, 2])
        notes_per_bar = self.rng.choice([3, 4, 5])
        
        for bar in range(motif_bars):
            bar_offset = bar * 4.0
            
            # Select rhythmic pattern
            if notes_per_bar == 3:
                positions = [0.0, 1.5, 3.0]
                durations = [0.8, 0.8, 1.0]
            elif notes_per_bar == 4:
                positions = [0.0, 1.0, 2.0, 3.0]
                durations = [0.5, 0.5, 0.5, 0.5]
            else:  # 5 notes
                positions = [0.0, 0.75, 2.0, 2.75, 3.5]
                durations = [0.4, 0.4, 0.4, 0.4, 0.4]
            
            # Select pitches from scale
            for i, pos in enumerate(positions):
                # Choose scale degree based on position (emphasize root on downbeats)
                if pos == 0.0:
                    degree = 0  # Root on downbeat
                elif i % 2 == 0:
                    degree = self.rng.choice([0, 2, 4])  # Triad notes
                else:
                    degree = self.rng.choice([1, 2, 3, 4, 5, 6])  # Any scale note
                    
                pitch = scale_notes[degree % len(scale_notes)]
                duration = durations[i] if i < len(durations) else 0.5
                velocity = self.rng.randint(80, 110)
                
                motif.append({
                    'pitch': pitch,
                    'start': bar_offset + pos,
                    'duration': duration,
                    'velocity': velocity
                })
        
        # Optionally repeat for 2-bar total
        if motif_bars == 1 and bars >= 2:
            repeated = []
            for bar in range(2):
                for note in motif:
                    repeated.append({
                        'pitch': note['pitch'],
                        'start': note['start'] + (bar * 4.0),
                        'duration': note['duration'],
                        'velocity': note['velocity']
                    })
            return repeated
            
        return motif
    
    def get_section_variation(self, section_kind):
        """
        Get theme variation for a specific section type.
        
        Args:
            section_kind: 'intro', 'build', 'drop', 'break', 'outro'
            
        Returns:
            List of note dicts representing the motif variation
        """
        if section_kind in self.variations:
            return self.variations[section_kind]
            
        variations = {
            'intro': self._create_intro_version(),
            'build': self._create_tension_version(),
            'drop': self._create_full_version(),
            'break': self._create_reduced_version(),
            'outro': self._create_degraded_version()
        }
        
        variation = variations.get(section_kind, self.base_motif)
        self.variations[section_kind] = variation
        return variation
    
    def _create_intro_version(self):
        """Partial motif, sparse - every other note."""
        return [note for i, note in enumerate(self.base_motif) if i % 2 == 0]
    
    def _create_tension_version(self):
        """Motif with added tension notes (anticipation pickups)."""
        tension = list(self.base_motif)
        scale_notes = self._get_scale_notes(octave=4)
        
        # Add anticipation notes before some main notes
        for i, note in enumerate(self.base_motif):
            if i % 2 == 1 and note['start'] > 0.5:
                tension_note = {
                    'pitch': note['pitch'] + self.rng.choice([1, 2, -1, -2]),
                    'start': max(0, note['start'] - 0.25),
                    'duration': 0.15,
                    'velocity': max(50, note['velocity'] - 20)
                }
                # Quantize to scale
                tension_note['pitch'] = self._quantize_to_scale(tension_note['pitch'], scale_notes)
                tension.append(tension_note)
                
        # Sort by start time
        tension.sort(key=lambda x: x['start'])
        return tension
    
    def _create_full_version(self):
        """Complete hook - original motif repeated/extended."""
        full = []
        # Repeat motif for 2 bars
        for bar in range(2):
            offset = bar * 4.0
            for note in self.base_motif:
                if note['start'] + offset < 8.0:
                    full.append({
                        'pitch': note['pitch'],
                        'start': note['start'] + offset,
                        'duration': note['duration'],
                        'velocity': note['velocity']
                    })
        return full
    
    def _create_reduced_version(self):
        """Response or minimal version - just key notes."""
        if len(self.base_motif) >= 3:
            # Keep first note and middle note
            return [self.base_motif[0], self.base_motif[len(self.base_motif) // 2]]
        elif len(self.base_motif) >= 1:
            return [self.base_motif[0]]
        return self.base_motif
    
    def _create_degraded_version(self):
        """Fading out - reduced velocity and held notes."""
        degraded = []
        for note in self.base_motif:
            degraded.append({
                'pitch': note['pitch'],
                'start': note['start'],
                'duration': note['duration'] * 1.5,  # Longer sustains
                'velocity': max(40, note['velocity'] - 30)  # Quieter
            })
        return degraded
    
    def _quantize_to_scale(self, pitch, scale_notes):
        """Quantize a pitch to the nearest scale note."""
        if pitch in scale_notes:
            return pitch
        return min(scale_notes, key=lambda x: abs(x - pitch))
    
    def motif_to_bass(self, motif, octave_offset=-2):
        """
        Extract bass line from motif (root notes, lower octave).
        
        Args:
            motif: List of note dicts from motif variation
            octave_offset: Octaves down (default -2 for bass range)
            
        Returns:
            List of note dicts for bass track
        """
        bass_notes = []
        scale_notes = self._get_scale_notes(octave=4)
        
        for note in motif:
            # Get root of the chord implied by this note (quantize to scale then drop)
            pitch = note['pitch']
            root_pitch = self._quantize_to_scale(pitch, scale_notes) + (octave_offset * 12)
            
            bass_notes.append({
                'pitch': root_pitch,
                'start': note['start'],
                'duration': 1.0,  # Longer sustains for bass
                'velocity': min(100, note['velocity'] + 10)
            })
            
        return bass_notes
    
    def motif_to_chords(self, motif, voicing='triad'):
        """
        Build chord progression from motif notes.
        
        Args:
            motif: List of note dicts from motif variation
            voicing: 'triad', 'seventh', or 'power'
            
        Returns:
            List of chord dicts with 'notes' (list of pitches), 'start', 'duration'
        """
        chords = []
        scale_notes = self._get_scale_notes(octave=4)
        
        for note in motif:
            root = self._quantize_to_scale(note['pitch'], scale_notes)
            
            # Build chord based on scale degrees
            if voicing == 'triad':
                # Major/minor triad based on scale
                chord_pitches = [
                    root,
                    root + 4 if self.scale == 'major' else root + 3,  # Third
                    root + 7  # Fifth
                ]
            elif voicing == 'seventh':
                # Seventh chord
                seventh = root + 10 if self.scale == 'minor' else root + 11
                chord_pitches = [root, root + 4, root + 7, seventh]
            else:  # power
                chord_pitches = [root, root + 7, root + 12]
            
            chords.append({
                'notes': chord_pitches,
                'start': note['start'],
                'duration': 2.0,  # Longer for chords
                'velocity': max(60, note['velocity'] - 20)
            })
            
        return chords
    
    def motif_to_lead(self, motif, embellishment_level=0.5):
        """
        Create lead melody from motif with embellishments.
        
        Args:
            motif: List of note dicts from motif variation
            embellishment_level: 0.0-1.0 amount of ornamentation
            
        Returns:
            List of note dicts for lead track
        """
        lead = []
        scale_notes = self._get_scale_notes(octave=5)  # Higher octave
        
        for i, note in enumerate(motif):
            # Transpose to higher octave
            transposed_pitch = note['pitch'] + 12
            transposed_pitch = self._quantize_to_scale(transposed_pitch, scale_notes)
            
            # Add original note
            lead.append({
                'pitch': transposed_pitch,
                'start': note['start'],
                'duration': note['duration'],
                'velocity': min(120, note['velocity'] + 15)  # Louder for lead
            })
            
            # Add passing notes based on embellishment level
            if embellishment_level > 0.3 and i < len(motif) - 1:
                next_note = motif[i + 1]
                if next_note['start'] - note['start'] > 0.5:
                    # Add passing note between longer gaps
                    passing_start = note['start'] + 0.25
                    passing_pitch = self._quantize_to_scale(
                        (transposed_pitch + (next_note['pitch'] + 12)) // 2,
                        scale_notes
                    )
                    lead.append({
                        'pitch': passing_pitch,
                        'start': passing_start,
                        'duration': 0.15,
                        'velocity': max(50, note['velocity'] - 20)
                    })
            
            # Add octave jumps for high embellishment
            if embellishment_level > 0.7 and self.rng.random() > 0.6:
                lead.append({
                    'pitch': transposed_pitch + 12,
                    'start': note['start'] + 0.1,
                    'duration': 0.2,
                    'velocity': max(60, note['velocity'] - 10)
                })
        
        # Sort by start time
        lead.sort(key=lambda x: x['start'])
        return lead
    
    def to_dict(self):
        """Serialize theme to dict for manifest."""
        return {
            'key': self.key,
            'scale': self.scale,
            'seed': self.seed,
            'base_motif_notes': [n['pitch'] for n in self.base_motif],
            'base_motif_length': len(self.base_motif),
            'variations_used': list(self.variations.keys())
        }



# =============================================================================

# PHRASE PLANNING - Hook-based melodic architecture

# =============================================================================



@dataclass
class Phrase:
    """A single melodic phrase/hook with mutation metadata."""
    start: float  # Bar position
    end: float
    kind: str  # 'hook', 'response', 'variation', 'fill'
    role: str  # 'synth', 'bass', 'pad', 'pluck', 'lead', etc.
    family: str  # 'pluck', 'pad', 'piano', 'keys', 'synth'
    instrument_hint: Dict  # From harmonic resolution
    mutation_type: str  # 'full', 'partial', 'sparse', 'tension', 'response', 'fade'
    notes: List[Dict]  # MIDI note data
    section_kind: str  # 'intro', 'build', 'drop', 'break', 'outro'
    
    def to_dict(self) -> Dict[str, Any]:
        """Serialize phrase to dict for manifest."""
        return {
            'start': self.start,
            'end': self.end,
            'kind': self.kind,
            'role': self.role,
            'family': self.family,
            'mutation': self.mutation_type,
            'mutation_type': self.mutation_type,
            'section_kind': self.section_kind,
            'note_count': len(self.notes),
            'pitch_range': self._get_pitch_range(),
            'instrument_hint': self.instrument_hint,
            'notes': list(self.notes),
        }

    @classmethod
    def from_dict(cls, data: Dict[str, Any]) -> 'Phrase':
        """Restore a phrase from serialized data."""
        if not isinstance(data, dict):
            raise TypeError("Phrase data must be a dict")

        return cls(
            start=float(data.get('start', 0.0)),
            end=float(data.get('end', 4.0)),
            kind=str(data.get('kind', 'hook')),
            role=str(data.get('role', 'pluck')),
            family=str(data.get('family', data.get('role', 'pluck'))).strip().lower(),
            instrument_hint=dict(data.get('instrument_hint', {}) or {}),
            mutation_type=str(data.get('mutation_type', data.get('mutation', 'full'))),
            notes=list(data.get('notes', []) or []),
            section_kind=str(data.get('section_kind', 'drop')),
        )
    
    def _get_pitch_range(self) -> Dict[str, int]:
        """Get min/max pitch of phrase."""
        if not self.notes:
            return {'min': 0, 'max': 0}
        pitches = [n.get('pitch', 60) for n in self.notes]
        return {'min': min(pitches), 'max': max(pitches)}



class PhrasePlan:
    """
    Plan of melodic phrases across song sections.
    
    Instead of thinking in long loops, this plans short hook phrases
    that mutate across sections while maintaining coherence.
    """
    
    # Mutation rules by section kind
    MUTATION_MAP = {
        'intro': 'sparse',      # Every other note
        'build': 'tension',     # Add anticipation
        'drop': 'full',         # Complete hook
        'break': 'response',    # Minimal, 2 key notes
        'outro': 'fade'         # Degraded
    }
    
    # Phrase kind by section
    PHRASE_KIND_MAP = {
        'intro': 'response',
        'build': 'variation',
        'drop': 'hook',
        'break': 'response',
        'outro': 'response'
    }
    
    # Default phrase roles by section
    PHRASE_ROLES = {
        'intro': ['pluck', 'pad'],
        'build': ['pluck', 'synth'],
        'drop': ['pluck', 'lead', 'synth'],
        'break': ['pad', 'pluck'],
        'outro': ['pad', 'pluck']
    }

    @staticmethod
    def _normalize_family_name(family: Optional[str]) -> Optional[str]:
        """Normalize family names to the lowercase tokens used by the generator."""
        if family is None:
            return None
        normalized = str(family).strip().lower()
        return normalized or None
    
    def __init__(self, base_motif: List[Dict], sections: List[Dict], 
                 key: str = 'Am', scale: str = 'minor', seed: int = None,
                 primary_harmonic_family: str = None):
        """
        Initialize phrase plan from base motif and sections.
        
        Args:
            base_motif: List of note dicts representing the core melodic idea
            sections: List of section dicts with 'kind', 'start_bar', 'end_bar'
            key: Musical key (e.g., 'Am', 'F#m')
            scale: Scale type ('minor', 'major', etc.)
            seed: Random seed for reproducibility
            primary_harmonic_family: LOCKED family for all phrases (e.g., 'pluck', 'pad')
        """
        self.base_motif = base_motif
        self.sections = sections
        self.key = key
        self.scale = scale
        self.seed = seed or random.randint(0, 10000)
        self.rng = random.Random(self.seed)
        self.phrases: List[Phrase] = []
        self.scale_notes = self._get_scale_notes()
        
        # FAMILY LOCK - primary family extracted from reference
        self.primary_harmonic_family = self._normalize_family_name(primary_harmonic_family)
        
        if self.primary_harmonic_family:
            logger.info(f"FAMILY_LOCK: Primary family set to {self.primary_harmonic_family}")
        
        # Generate the phrase sequence
        self._generate_phrases()
        
        # Verify family coherence if lock is set
        if self.primary_harmonic_family:
            self._verify_family_coherence()
    
    def _get_scale_notes(self, octave: int = 4) -> List[int]:
        """Get MIDI notes for the current key/scale."""
        root_note = self.key[:-1] if len(self.key) > 1 else self.key
        note_names = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
        
        # Normalize note names
        root_note = root_note.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')
        root_note = root_note.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')
        
        try:
            note_idx = note_names.index(root_note.upper())
            root_midi = (octave + 1) * 12 + note_idx
        except ValueError:
            root_midi = 60  # Default C4
        
        scale_intervals = SCALES.get(self.scale, SCALES['minor'])
        return [root_midi + interval for interval in scale_intervals]
    
    def _generate_phrases(self):
        """Generate phrase sequence from base motif across all sections."""
        for i, section in enumerate(self.sections):
            section_kind = section.get('kind', 'drop')
            start_bar = float(section.get('start_bar', section.get('start', i * 16)))
            end_bar = float(section.get('end_bar', section.get('end', start_bar + 16)))
            
            # Determine mutation for this section
            mutation = self._get_section_mutation(section_kind)
            
            # Get phrase kind and roles
            phrase_kind = self.PHRASE_KIND_MAP.get(section_kind, 'hook')
            roles = self.PHRASE_ROLES.get(section_kind, ['pluck'])
            
            # Determine harmonic instrument family from context
            family = self._determine_family(section_kind, i)
            
            # Create primary phrase for this section
            phrase = Phrase(
                start=start_bar,
                end=end_bar,
                kind=phrase_kind,
                role=roles[0] if roles else 'pluck',
                family=family,
                instrument_hint=self._get_instrument_hint(family, section_kind),
                mutation_type=mutation,
                notes=self._mutate_motif(self.base_motif, mutation),
                section_kind=section_kind
            )
            
            self.phrases.append(phrase)
            
            # Add secondary phrase for certain sections
            if section_kind in ['drop', 'build'] and len(roles) > 1:
                secondary_mutation = 'full' if section_kind == 'drop' else 'tension'
                secondary = Phrase(
                    start=start_bar,
                    end=end_bar,
                    kind='variation' if section_kind == 'drop' else 'hook',
                    role=roles[1],
                    family=family,
                    instrument_hint=self._get_instrument_hint(roles[1], section_kind),
                    mutation_type=secondary_mutation,
                    notes=self._mutate_motif(self.base_motif, secondary_mutation, octave_offset=1),
                    section_kind=section_kind
                )
                self.phrases.append(secondary)
    
    def _get_section_mutation(self, section_kind: str) -> str:
        """Determine how to mutate motif for this section."""
        return self.MUTATION_MAP.get(section_kind, 'full')
    
    def _determine_family(self, section_kind: str, section_index: int) -> str:
        """Determine instrument family based on section and position.
        
        PRIORITY 1: Use locked family if available (from reference analysis)
        PRIORITY 2: Deterministic fallback based on section index
        """
        # PRIORITY 1: Use locked family if available
        if self.primary_harmonic_family:
            # All sections use the SAME family - mutations happen in density/energy, not timbre
            return self.primary_harmonic_family
        
        # PRIORITY 2: Deterministic fallback (no random drift)
        families = ['pluck', 'pad', 'piano', 'keys', 'synth']
        
        # Weight by section kind - but deterministically
        if section_kind == 'drop':
            priority_families = ['pluck', 'synth']
        elif section_kind == 'break':
            priority_families = ['pad', 'pluck']
        elif section_kind == 'build':
            priority_families = ['synth', 'pluck']
        elif section_kind == 'intro':
            priority_families = ['pluck', 'pad']
        else:  # outro
            priority_families = ['pad', 'pluck']
        
        # Deterministic selection based on section index
        return priority_families[section_index % len(priority_families)]
    
    def _verify_family_coherence(self):
        """Verify all phrases use the same family when locked."""
        if not self.primary_harmonic_family or not self.phrases:
            return
        
        families = set(p.family for p in self.phrases)
        if len(families) > 1:
            logger.warning(f"FAMILY_DRIFT_DETECTED: {families} - expected all {self.primary_harmonic_family}")
        else:
            logger.info(f"FAMILY_COHERENT: All {len(self.phrases)} phrases use {self.primary_harmonic_family}")
    
    def _get_instrument_hint(self, family: str, section_kind: str) -> Dict[str, Any]:
        """Generate instrument selection hints."""
        hints = {
            'pluck': {
                'brightness': 'medium',
                'attack': 'fast',
                'sustain': 'short',
                'recommended_adsr': [0.01, 0.3, 0.4, 0.3]
            },
            'pad': {
                'brightness': 'warm',
                'attack': 'slow',
                'sustain': 'long',
                'recommended_adsr': [0.2, 0.5, 0.6, 1.0]
            },
            'piano': {
                'brightness': 'natural',
                'attack': 'medium',
                'sustain': 'medium',
                'recommended_adsr': [0.02, 0.4, 0.3, 0.4]
            },
            'keys': {
                'brightness': 'bright',
                'attack': 'fast',
                'sustain': 'medium',
                'recommended_adsr': [0.01, 0.2, 0.5, 0.3]
            },
            'synth': {
                'brightness': 'bright' if section_kind == 'drop' else 'medium',
                'attack': 'fast',
                'sustain': 'long' if section_kind == 'drop' else 'medium',
                'recommended_adsr': [0.02, 0.3, 0.5, 0.5]
            },
            'lead': {
                'brightness': 'bright',
                'attack': 'fast',
                'sustain': 'long',
                'recommended_adsr': [0.01, 0.2, 0.6, 0.4]
            }
        }
        return hints.get(family, hints['pluck'])
    
    def _mutate_motif(self, motif: List[Dict], mutation: str, octave_offset: int = 0) -> List[Dict]:
        """
        Apply mutation to base motif.
        
        Args:
            motif: Base motif notes
            mutation: Mutation type
            octave_offset: Octave transposition
        
        Returns:
            Mutated note list
        """
        if not motif:
            return []
        
        # Apply octave offset first
        transposed = []
        for note in motif:
            transposed.append({
                'pitch': note.get('pitch', 60) + (octave_offset * 12),
                'start': note.get('start', 0.0),
                'duration': note.get('duration', 0.5),
                'velocity': note.get('velocity', 100)
            })
        
        # Apply mutation
        if mutation == 'sparse':
            # Keep every other note
            return [transposed[i] for i in range(0, len(transposed), 2)]
        
        elif mutation == 'tension':
            # Add anticipation pickups
            result = list(transposed)
            for i, note in enumerate(transposed[:-1]):
                next_note = transposed[i + 1]
                if next_note['start'] > note['start'] + 0.5:
                    # Add passing note
                    passing_pitch = self._quantize_to_scale(
                        note['pitch'] + self.rng.choice([1, 2, -1, -2])
                    )
                    result.append({
                        'pitch': passing_pitch,
                        'start': note['start'] + note['duration'],
                        'duration': 0.25,
                        'velocity': max(50, note['velocity'] - 20)
                    })
            return sorted(result, key=lambda x: x['start'])
        
        elif mutation == 'full':
            # Double the motif for 2-bar phrase
            result = []
            for bar in range(2):
                offset = bar * 4.0
                for note in transposed:
                    if note['start'] + offset < 8.0:
                        result.append({
                            'pitch': note['pitch'],
                            'start': note['start'] + offset,
                            'duration': note['duration'],
                            'velocity': note['velocity']
                        })
            return result
        
        elif mutation == 'response':
            # Just first and last note
            if len(transposed) >= 2:
                return [transposed[0], transposed[-1]]
            return transposed[:1] if transposed else []
        
        elif mutation == 'fade':
            # Reduced velocity and longer sustains
            return [{
                'pitch': n['pitch'],
                'start': n['start'],
                'duration': n['duration'] * 1.5,
                'velocity': max(40, n['velocity'] - 30)
            } for n in transposed]
        
        # Default: return transposed motif
        return transposed
    
    def _quantize_to_scale(self, pitch: int) -> int:
        """Quantize pitch to nearest scale note."""
        if pitch in self.scale_notes:
            return pitch
        # Find nearest
        octave = (pitch // 12) - 1
        local_pitch = pitch % 12
        root = self.scale_notes[0] % 12
        # Calculate relative position
        intervals = SCALES.get(self.scale, SCALES['minor'])
        closest = min(intervals, key=lambda x: abs(x - ((local_pitch - root) % 12)))
        return (octave + 1) * 12 + ((root + closest) % 12)
    
    def get_phrases_for_section(self, section_kind: str) -> List[Phrase]:
        """Get all phrases for a specific section kind."""
        return [p for p in self.phrases if p.section_kind == section_kind]
    
    def get_primary_hook(self) -> Optional[Phrase]:
        """Get the primary hook phrase (drop)."""
        for phrase in self.phrases:
            if phrase.kind == 'hook' and phrase.section_kind == 'drop':
                return phrase
        return self.phrases[0] if self.phrases else None
    
    def to_dict(self) -> Dict[str, Any]:
        """Serialize phrase plan to dict for manifest."""
        return {
            'key': self.key,
            'scale': self.scale,
            'seed': self.seed,
            'primary_harmonic_family': self.primary_harmonic_family,
            'base_motif': list(self.base_motif),
            'base_motif_notes': [n['pitch'] for n in self.base_motif],
            'base_motif_length': len(self.base_motif),
            'phrase_count': len(self.phrases),
            'sections_covered': len(self.sections),
            'sections': list(self.sections),
            'phrases': [p.to_dict() for p in self.phrases],
            'mutation_summary': self._summarize_mutations()
        }
    
    def _summarize_mutations(self) -> Dict[str, int]:
        """Count mutations by type."""
        counts = {}
        for phrase in self.phrases:
            counts[phrase.mutation_type] = counts.get(phrase.mutation_type, 0) + 1
        return counts
    
    @classmethod
    def from_musical_theme(cls, theme: 'MusicalThemeGenerator', sections: List[Dict],
                           primary_harmonic_family: str = None) -> 'PhrasePlan':
        """Create phrase plan from an existing MusicalThemeGenerator.
        
        Args:
            theme: MusicalThemeGenerator instance with base_motif
            sections: List of section dicts
            primary_harmonic_family: Optional locked family for all phrases
        """
        return cls(
            base_motif=theme.base_motif,
            sections=sections,
            key=theme.key,
            scale=theme.scale,
            seed=theme.seed,
            primary_harmonic_family=primary_harmonic_family
        )

    @classmethod
    def from_dict(cls, data: Dict[str, Any], sections_override: Optional[List[Dict[str, Any]]] = None) -> 'PhrasePlan':
        """Restore a phrase plan from serialized data."""
        if not isinstance(data, dict):
            raise TypeError("PhrasePlan data must be a dict")

        base_motif = list(data.get('base_motif', []) or [])
        if not base_motif:
            base_motif_notes = list(data.get('base_motif_notes', []) or [])
            base_motif = [
                {
                    'pitch': int(pitch),
                    'start': float(index),
                    'duration': 1.0,
                    'velocity': 100,
                }
                for index, pitch in enumerate(base_motif_notes)
            ]

        sections = list(data.get('sections', []) or sections_override or [])
        if not sections:
            sections = list(sections_override or [])

        plan = cls(
            base_motif=base_motif,
            sections=sections,
            key=str(data.get('key', 'Am')),
            scale=str(data.get('scale', 'minor')),
            seed=data.get('seed'),
            primary_harmonic_family=data.get('primary_harmonic_family'),
        )

        phrase_dicts = list(data.get('phrases', []) or [])
        if phrase_dicts:
            plan.phrases = [Phrase.from_dict(item) for item in phrase_dicts if isinstance(item, dict)]

        return plan



# =============================================================================

# DRUM PATTERN BANKS - Expanded Section-Specific Variants (11+ kick, 10+ clap, 8+ hat)

# =============================================================================



# Section-specific drum variants mapping - EXPANDED with 11+ kick, 10+ clap, 8+ hat variants

DRUM_SECTION_VARIANTS = {

    'intro': {

        # KICK: 11 variants - minimal, ghost notes, filtered, etc.

        'kick': ['sparse', 'minimal', 'foreshadow', 'hint', 'ghost', 'filtered', 'subtle', 'pulse', 'sub_bass', 'tick', 'heartbeat'],

        # CLAP: 10 variants

        'clap': ['absent', 'hint', 'ghost', 'filtered', 'reverb_tail', 'minimal', 'subtle', 'single', 'distant', 'echo'],

        # HAT: 8+ variants

        'hat_closed': ['sparse', 'ghost', 'whisper', 'filtered', 'minimal', 'reverb_tail', 'subtle', 'tick'],

        'hat_open': ['absent', 'hint', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'tick', 'single'],

        'perc': ['minimal', 'atmos', 'ghost', 'subtle', 'filtered', 'tick', 'reverb_tail', 'sparse'],

        'ride': ['absent', 'hint', 'subtle', 'minimal', 'filtered', 'ghost'],

        'top_loop': ['absent', 'hint', 'filtered', 'minimal', 'subtle', 'ghost'],

        'snare_fill': ['absent', 'hint', 'ghost', 'minimal'],

        'tom_fill': ['absent', 'hint', 'ghost', 'filtered'],

    },

    'build': {

        # KICK: 11 variants - building energy

        'kick': ['building', 'pressure', 'rising', 'tension', 'accelerate', 'filter_sweep', 'drive_up', 'tighten', 'fill_preparation', 'intensity', 'impact_build'],

        # CLAP: 10 variants

        'clap': ['building', 'anticipate', 'roll_in', 'intensify', 'echo_build', 'filter_sweep', 'layering', 'reverb_up', 'drive_up', 'accelerate'],

        'hat_closed': ['building', 'open_up', 'hyper', 'intensify', 'filter_sweep', 'accelerate', 'reverb_up', 'layering'],

        'hat_open': ['building', 'tease', 'accent', 'filter_sweep', 'intensify', 'fill_preparation', 'open_build'],

        'perc': ['layering', 'tension', 'build_up', 'intensify', 'accelerate', 'filter_sweep', 'reverb_up', 'drive_up'],

        'ride': ['building', 'rising', 'intensify', 'filter_sweep', 'reverb_up', 'accelerate'],

        'top_loop': ['building', 'energy', 'intensify', 'filter_sweep', 'drive_up', 'layering'],

        'snare_fill': ['rolling', 'tension', 'accelerate', 'intensify', 'fill_preparation'],

        'tom_fill': ['rising', 'fill', 'intensify', 'accelerate', 'fill_preparation'],

    },

    'drop': {

        # KICK: 11 variants - full energy patterns

        'kick': ['full', 'punch', 'four_on_floor', 'groove', 'impact', 'heavy', 'driving', 'tight', 'big_room', 'club', 'techno_thump'],

        # CLAP: 10 variants

        'clap': ['full', 'backbeat', 'syncopated', 'punch', 'big', 'layered', 'room', 'tight', 'crisp', 'slap'],

        'hat_closed': ['full', 'groove', 'offbeat', 'shuffle', 'tight', 'driving', 'punchy', 'crisp'],

        'hat_open': ['full', 'offbeat', 'groove', 'accent', 'big', 'room', 'open_drive', 'shuffle'],

        'perc': ['full', 'layered', 'groove', 'latin', 'tribal', 'driving', 'tight', 'energetic'],

        'ride': ['full', 'groove', 'energy', 'driving', 'tight', 'shimmer'],

        'top_loop': ['full', 'energy', 'layered', 'driving', 'tight', 'groove'],

        'snare_fill': ['drop_hit', 'fill', 'impact', 'big', 'accent'],

        'tom_fill': ['drop_hit', 'fill', 'impact', 'big', 'accent'],

    },

    'break': {

        # KICK: 11 variants - stripped down

        'kick': ['sparse', 'absent', 'minimal', 'foreshadow', 'ghost', 'filtered', 'subtle', 'heartbeat', 'pulse', 'distant', 'reverb_only'],

        # CLAP: 10 variants

        'clap': ['sparse', 'offbeat', 'ghost', 'filtered', 'reverb_tail', 'minimal', 'subtle', 'distant', 'echo', 'single'],

        'hat_closed': ['open', 'sparse', 'atmos', 'filtered', 'minimal', 'reverb_tail', 'subtle', 'ghost'],

        'hat_open': ['sparse', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'subtle', 'atmos', 'distant'],

        'perc': ['minimal', 'atmos', 'filtered', 'ghost', 'reverb_tail', 'subtle', 'sparse', 'distant'],

        'ride': ['sparse', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'subtle'],

        'top_loop': ['filtered', 'hint', 'minimal', 'ghost', 'reverb_tail', 'subtle'],

        'snare_fill': ['tension', 'ghost', 'minimal', 'filtered', 'echo'],

        'tom_fill': ['tension', 'ghost', 'minimal', 'filtered', 'echo'],

    },

    'outro': {

        # KICK: 11 variants - fading out

        'kick': ['fading', 'minimal', 'sparse', 'strip_down', 'reverb_tail', 'heartbeat', 'subtle', 'distant', 'filtered', 'pulse', 'fade'],

        # CLAP: 10 variants

        'clap': ['fading', 'sparse', 'last_hit', 'minimal', 'reverb_tail', 'distant', 'echo', 'subtle', 'ghost', 'filtered'],

        'hat_closed': ['fading', 'open', 'minimal', 'reverb_tail', 'subtle', 'sparse', 'ghost', 'filtered'],

        'hat_open': ['fading', 'last_hit', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'distant', 'filtered'],

        'perc': ['fading', 'minimal', 'strip_down', 'reverb_tail', 'subtle', 'sparse', 'ghost', 'filtered'],

        'ride': ['fading', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'filtered'],

        'top_loop': ['fading', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'filtered'],

        'snare_fill': ['end_fill', 'absent', 'minimal', 'reverb_tail', 'ghost'],

        'tom_fill': ['end_fill', 'absent', 'minimal', 'reverb_tail', 'ghost'],

    },

}



# Expanded drum pattern generators for section variation

DRUM_PATTERN_BANKS = {

    'kick': {

        'four_on_floor': [0.0, 1.0, 2.0, 3.0],

        'sparse': [0.0, 2.0],

        'minimal': [0.0],

        'foreshadow': [0.0, 3.5],

        'hint': [0.0, 2.5],

        'building': [0.0, 1.0, 2.0, 3.0, 3.5],

        'pressure': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'rising': [0.0, 1.0, 2.0, 2.75, 3.0, 3.25, 3.5, 3.75],

        'tension': [0.0, 0.25, 1.0, 1.5, 2.0, 2.75, 3.0, 3.25, 3.5],

        'full': [0.0, 1.0, 2.0, 3.0],

        'punch': [0.0, 0.25, 1.0, 2.0, 3.0],

        'groove': [0.0, 0.75, 1.0, 1.75, 2.0, 2.75, 3.0, 3.75],

        'impact': [0.0, 0.25, 0.5, 1.0, 2.0, 3.0],

        'fading': [0.0, 2.0],

        'strip_down': [0.0],

        'absent': [],

    },

    'clap': {

        'backbeat': [1.0, 3.0],

        'sparse': [1.0],

        'hint': [3.0],

        'building': [1.0, 2.5, 3.0],

        'anticipate': [1.0, 2.0, 2.75, 3.0, 3.5],

        'roll_in': [0.75, 1.0, 1.25, 1.5, 2.75, 3.0, 3.25, 3.5],

        'full': [1.0, 3.0],

        'syncopated': [0.75, 1.0, 2.75, 3.0],

        'offbeat': [1.5, 3.5],

        'punch': [0.75, 1.0, 1.25, 2.75, 3.0, 3.25],

        'ghost': [3.0],

        'last_hit': [1.0],

        'fading': [1.0],

        'absent': [],

    },

    'hat_closed': {

        'offbeat': [0.5, 1.5, 2.5, 3.5],

        'sparse': [0.5, 2.5],

        'ghost': [0.25, 1.25, 2.25, 3.25],

        'whisper': [0.75, 1.75, 2.75, 3.75],

        'building': [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'open_up': [0.5, 0.75, 1.5, 1.75, 2.5, 2.75, 3.5, 3.75],

        'hyper': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75],

        'full': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'groove': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'shuffle': [0.0, 0.33, 0.66, 1.0, 1.33, 1.66, 2.0, 2.33, 2.66, 3.0, 3.33, 3.66],

        'filtered': [0.5, 1.5, 2.5, 3.5],

        'energy': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'fading': [0.5, 2.5],

        'minimal': [0.5],

    },

    'hat_open': {

        'sparse': [2.0],

        'building': [1.5, 2.5, 3.0],

        'full': [0.0, 2.0],

        'offbeat': [1.5, 3.5],

        'tease': [3.5],

        'fading': [2.0],

        'last_hit': [3.5],

        'hint': [2.0],

        'absent': [],

    },

    'perc': {

        'minimal': [1.5],

        'atmos': [0.75, 2.75],

        'ghost': [0.25, 2.25],

        'layering': [0.5, 1.5, 2.5, 3.5],

        'tension': [0.25, 1.25, 2.25, 3.25],

        'build_up': [0.5, 1.0, 2.0, 3.0, 3.5],

        'full': [0.5, 1.0, 1.5, 2.5, 3.0, 3.5],

        'layered': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],

        'groove': [0.5, 1.0, 2.0, 2.5, 3.5],

        'latin': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],

        'tribal': [0.0, 0.5, 1.25, 1.75, 2.5, 3.0, 3.75],

        'filtered': [0.5, 2.5],

        'fading': [1.5],

        'strip_down': [0.0],

        'hint': [2.0],

    },

    'ride': {

        'sparse': [0.0, 2.0],

        'building': [0.0, 1.0, 2.0, 3.0],

        'rising': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],

        'full': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'groove': [0.0, 0.25, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'energy': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5],

        'filtered': [0.0, 2.0],

        'fading': [0.0],

        'minimal': [0.0],

        'absent': [],

    },

    'top_loop': {

        'minimal': [0.25, 1.25, 2.25, 3.25],

        'energy': [0.0, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 2.25, 2.5, 3.0, 3.25, 3.5],

        'building': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],

        'full': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'layered': [0.25, 0.5, 0.75, 1.25, 1.5, 1.75, 2.25, 2.5, 2.75, 3.25, 3.5, 3.75],

        'filtered': [0.5, 1.5, 2.5, 3.5],

        'fading': [0.5, 2.5],

        'hint': [1.5, 3.5],

        'absent': [],

    },

    'snare_fill': {

        'rolling': [2.0, 2.125, 2.25, 2.375, 2.5, 2.625, 2.75, 2.875, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],

        'tension': [3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],

        'drop_hit': [0.0],

        'fill': [3.0, 3.25, 3.5, 3.75],

        'end_fill': [0.0, 0.25, 0.5, 0.75],

        'absent': [],

    },

    'tom_fill': {

        'rising': [3.0, 3.2, 3.4, 3.6, 3.8],

        'fill': [3.0, 3.125, 3.25, 3.375, 3.5],

        'drop_hit': [0.0],

        'tension': [3.5, 3.625, 3.75, 3.875],

        'end_fill': [0.0, 0.2, 0.4, 0.6],

        'absent': [],

    },

}



# Section-specific bass variants - EXPANDED

BASS_SECTION_VARIANTS = {

    'intro': ['subtle', 'hint', 'foreshadow', 'ghost', 'minimal'],

    'build': ['rising', 'tension', 'anticipate', 'building', 'pressure'],

    'drop': ['full', 'punch', 'groove', 'deep', 'impact', 'energy', 'rolling'],

    'break': ['sparse', 'minimal', 'atmos', 'filtered', 'foreshadow'],

    'outro': ['fading', 'minimal', 'subtle', 'strip_down'],

}



# Expanded bass pattern templates (relative positions in 4-bar cycle)

BASS_PATTERN_BANKS = {

    'anchor': {

        'positions': [0.0, 1.0, 2.0, 3.0],

        'durations': [0.5, 0.5, 0.5, 0.5],

        'style': 'root_heavy'

    },

    'subtle': {

        'positions': [0.0, 2.0],

        'durations': [0.3, 0.3],

        'style': 'minimal'

    },

    'hint': {

        'positions': [0.0, 3.5],

        'durations': [0.25, 0.25],

        'style': 'foreshadow'

    },

    'foreshadow': {

        'positions': [0.0, 1.0, 3.0, 3.5],

        'durations': [0.4, 0.3, 0.4, 0.3],

        'style': 'building'

    },

    'ghost': {

        'positions': [0.5, 2.5],

        'durations': [0.2, 0.2],

        'style': 'minimal'

    },

    'rising': {

        'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5, 0.4],

        'style': 'ascending'

    },

    'tension': {

        'positions': [0.0, 0.75, 1.5, 2.25, 3.0, 3.5],

        'durations': [0.5, 0.25, 0.5, 0.25, 0.5, 0.3],

        'style': 'syncopated'

    },

    'anticipate': {

        'positions': [0.0, 1.0, 2.0, 2.75, 3.0, 3.25, 3.5],

        'durations': [0.5, 0.5, 0.4, 0.2, 0.4, 0.2, 0.4],

        'style': 'building'

    },

    'building': {

        'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75],

        'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.3, 0.2, 0.3, 0.2],

        'style': 'ascending'

    },

    'pressure': {

        'positions': [0.0, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75],

        'durations': [0.3, 0.2, 0.3, 0.2, 0.4, 0.4, 0.4, 0.4, 0.3, 0.2, 0.3, 0.2],

        'style': 'intense'

    },

    'full': {

        'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.5, 0.4, 0.5, 0.4, 0.5, 0.4, 0.5, 0.4],

        'style': 'groove'

    },

    'punch': {

        'positions': [0.0, 0.25, 1.0, 2.0, 3.0],

        'durations': [0.6, 0.2, 0.5, 0.5, 0.5],

        'style': 'punchy'

    },

    'groove': {

        'positions': [0.0, 0.25, 0.75, 1.0, 1.75, 2.0, 2.75, 3.0, 3.5],

        'durations': [0.4, 0.2, 0.3, 0.4, 0.3, 0.4, 0.3, 0.4, 0.3],

        'style': 'syncopated'

    },

    'deep': {

        'positions': [0.0, 1.0, 2.0, 3.0],

        'durations': [0.8, 0.8, 0.8, 0.8],

        'style': 'sub'

    },

    'impact': {

        'positions': [0.0, 0.5, 1.5, 2.0, 3.0, 3.5],

        'durations': [0.6, 0.4, 0.3, 0.5, 0.5, 0.4],

        'style': 'punchy'

    },

    'energy': {

        'positions': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.4, 0.25, 0.4, 0.5, 0.4, 0.5, 0.4, 0.5, 0.4],

        'style': 'driving'

    },

    'rolling': {

        'positions': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75],

        'durations': [0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15],

        'style': 'rolling'

    },

    'sparse': {

        'positions': [0.0, 2.0],

        'durations': [0.4, 0.4],

        'style': 'minimal'

    },

    'minimal': {

        'positions': [0.0],

        'durations': [0.3],

        'style': 'hint'

    },

    'atmos': {

        'positions': [0.0, 3.0],

        'durations': [0.6, 0.4],

        'style': 'atmospheric'

    },

    'filtered': {

        'positions': [0.0, 1.5, 2.5],

        'durations': [0.4, 0.3, 0.3],

        'style': 'filtered'

    },

    'fading': {

        'positions': [0.0, 2.0],

        'durations': [0.5, 0.3],

        'style': 'decay'

    },

    'strip_down': {

        'positions': [0.0],

        'durations': [0.25],

        'style': 'minimal'

    },

    'bounce': {

        'positions': [0.0, 0.5, 1.5, 2.0, 2.5, 3.5],

        'durations': [0.4, 0.3, 0.4, 0.4, 0.3, 0.4],

        'style': 'bouncy'

    },

    'syncopated': {

        'positions': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],

        'durations': [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2],

        'style': 'offbeat'

    },

}



# Expanded fill patterns for section transitions

FILL_PATTERNS = {

    'drum_fill_4bar': {

        'roles': ['snare', 'kick', 'hat'],

        'pattern': {

            'snare': [3.0, 3.25, 3.5, 3.75],

            'kick': [3.5],

            'hat': [3.0, 3.5]

        },

        'velocities': {'snare': 100, 'kick': 90, 'hat': 70}

    },

    'drum_fill_2bar': {

        'roles': ['snare', 'hat'],

        'pattern': {

            'snare': [1.5, 1.75],

            'hat': [1.5]

        },

        'velocities': {'snare': 95, 'hat': 65}

    },

    'snare_roll': {

        'roles': ['snare'],

        'pattern': {

            'snare': [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25, 1.375, 1.5, 1.625, 1.75, 1.875]

        },

        'velocities': {'snare': 85}

    },

    'hat_open_build': {

        'roles': ['hat_open'],

        'pattern': {

            'hat_open': [0.0, 0.5, 1.0, 1.5, 2.0, 2.25, 2.5, 2.75, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875]

        },

        'velocities': {'hat_open': 75}

    },

    'kick_drop': {

        'roles': ['kick'],

        'pattern': {

            'kick': [0.0]

        },

        'velocities': {'kick': 127}

    },

    'crash_impact': {

        'roles': ['crash'],

        'pattern': {

            'crash': [0.0]

        },

        'velocities': {'crash': 100}

    },

    'snare_roll_build': {

        'roles': ['snare', 'hat'],

        'pattern': {

            'snare': [2.0, 2.25, 2.5, 2.75, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],

            'hat': [2.0, 2.5, 3.0, 3.5]

        },

        'velocities': {'snare': 88, 'hat': 70}

    },

    'tom_build': {

        'roles': ['tom_fill'],

        'pattern': {

            'tom_fill': [2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8]

        },

        'velocities': {'tom_fill': 90}

    },

    'full_impact': {

        'roles': ['kick', 'snare', 'crash'],

        'pattern': {

            'kick': [0.0],

            'snare': [0.0, 0.25],

            'crash': [0.0]

        },

        'velocities': {'kick': 127, 'snare': 110, 'crash': 105}

    },

    'hat_tension': {

        'roles': ['hat_closed'],

        'pattern': {

            'hat_closed': [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25, 1.375, 1.5, 1.625, 1.75, 1.875]

        },

        'velocities': {'hat_closed': 72}

    },

    'percussion_fill': {

        'roles': ['perc'],

        'pattern': {

            'perc': [0.5, 0.75, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5]

        },

        'velocities': {'perc': 78}

    },

    'minimal_drop': {

        'roles': ['kick'],

        'pattern': {

            'kick': [0.0]

        },

        'velocities': {'kick': 120}

    },

    'build_tension': {

        'roles': ['snare', 'hat_closed', 'kick'],

        'pattern': {

            'snare': [2.5, 2.75, 3.0, 3.25, 3.5, 3.75],

            'hat_closed': [2.0, 2.5, 3.0, 3.5],

            'kick': [0.0]

        },

        'velocities': {'snare': 92, 'hat_closed': 68, 'kick': 95}

    },

    'outro_fade': {

        'roles': ['hat_closed', 'perc'],

        'pattern': {

            'hat_closed': [0.0, 0.5, 1.0],

            'perc': [0.25, 0.75, 1.25]

        },

        'velocities': {'hat_closed': 80, 'perc': 70}

    },

}



# Expanded transition events between sections

TRANSITION_EVENTS = {

    ('intro', 'build'): ['hat_tension', 'hat_open_build'],

    ('build', 'drop'): ['full_impact', 'crash_impact', 'kick_drop', 'snare_roll_build'],

    ('drop', 'break'): ['drum_fill_4bar', 'percussion_fill'],

    ('break', 'build'): ['hat_tension', 'hat_open_build'],

    ('break', 'drop'): ['crash_impact', 'kick_drop', 'full_impact'],

    ('drop', 'outro'): ['drum_fill_2bar', 'outro_fade'],

    ('outro', 'end'): ['minimal_drop'],

}



# Rules for preventing transition overcrowding

TRANSITION_DENSITY_RULES = {

    # Max fills per section kind

    'max_fills_by_section': {

        'intro': 1,      # Minimal fills in intro

        'build': 3,      # More fills for tension

        'drop': 2,       # Moderate fills

        'break': 2,      # Sparse

        'outro': 1,      # Minimal

    },



    # Events that should not stack together

    'exclusive_events': [

        {'crash_impact', 'kick_drop'},  # Don't stack impact events

        {'drum_fill_4bar', 'snare_roll'},  # Choose one drum fill

    ],



    # Minimum distance between same-type fills (in beats)

    'min_distance_same_type': {

        'crash_impact': 8.0,

        'kick_drop': 16.0,

        'snare_roll': 4.0,

    }

}



# Section-specific melodic variants - EXPANDED

MELODIC_SECTION_VARIANTS = {

    'intro': ['subtle', 'foreshadow', 'atmospheric', 'ghost', 'hint'],

    'build': ['rising', 'tension', 'anticipate', 'building', 'energy'],

    'drop': ['hook', 'anthem', 'full', 'punchy', 'impact', 'driving'],

    'break': ['sparse', 'minimal', 'ethereal', 'filtered', 'atmospheric'],

    'outro': ['fading', 'echo', 'minimal', 'strip_down', 'decay'],

}



# Expanded melodic pattern templates

MELODIC_PATTERN_BANKS = {

    'motif': {

        'intervals': [0, 4, 7, 0],

        'rhythm': [0.0, 0.5, 1.0, 1.5],

        'durations': [0.4, 0.3, 0.4, 0.3],

        'style': 'repeating'

    },

    'subtle': {

        'intervals': [0, 0],

        'rhythm': [0.0, 2.0],

        'durations': [0.3, 0.3],

        'style': 'minimal'

    },

    'foreshadow': {

        'intervals': [0, 4, 0],

        'rhythm': [0.0, 1.0, 3.5],

        'durations': [0.4, 0.3, 0.5],

        'style': 'hint'

    },

    'atmospheric': {

        'intervals': [0, 2, 4, 5, 7],

        'rhythm': [0.0, 0.8, 1.6, 2.4, 3.2],

        'durations': [0.8, 0.7, 0.6, 0.5, 0.4],

        'style': 'pad'

    },

    'ghost': {

        'intervals': [0, 7],

        'rhythm': [0.5, 2.5],

        'durations': [0.2, 0.2],

        'style': 'minimal'

    },

    'hint': {

        'intervals': [0, 5],

        'rhythm': [0.0, 3.0],

        'durations': [0.25, 0.25],

        'style': 'minimal'

    },

    'rising': {

        'intervals': [0, 2, 4, 5, 7, 9, 11, 12],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.4, 0.35, 0.4, 0.35, 0.4, 0.35, 0.5, 0.4],

        'style': 'ascending'

    },

    'tension': {

        'intervals': [0, 1, 0, 1, 2, 1, 0],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],

        'durations': [0.3, 0.2, 0.3, 0.2, 0.3, 0.2, 0.5],

        'style': 'chromatic'

    },

    'anticipate': {

        'intervals': [0, 4, 7, 9, 12],

        'rhythm': [0.0, 1.0, 2.0, 3.0, 3.75],

        'durations': [0.5, 0.4, 0.5, 0.3, 0.5],

        'style': 'buildup'

    },

    'building': {

        'intervals': [0, 2, 4, 5, 7, 9, 11],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.75, 3.5],

        'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5],

        'style': 'ascending'

    },

    'energy': {

        'intervals': [0, 4, 7, 9, 12, 14],

        'rhythm': [0.0, 0.25, 0.75, 1.25, 2.0, 2.75],

        'durations': [0.3, 0.25, 0.3, 0.25, 0.4, 0.5],

        'style': 'driving'

    },

    'hook': {

        'intervals': [0, 4, 7, 4, 0, 4, 7, 12],

        'rhythm': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75],

        'durations': [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.3],

        'style': 'catchy'

    },

    'anthem': {

        'intervals': [0, 4, 7, 12, 11, 7, 4, 0],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.4, 0.4, 0.4, 0.5, 0.4, 0.4, 0.4, 0.5],

        'style': 'big'

    },

    'full': {

        'intervals': [0, 4, 7, 5, 4, 2, 0],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],

        'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5],

        'style': 'melodic'

    },

    'punchy': {

        'intervals': [0, 7, 0, 12],

        'rhythm': [0.0, 0.25, 0.5, 0.75],

        'durations': [0.15, 0.15, 0.15, 0.2],

        'style': 'staccato'

    },

    'impact': {

        'intervals': [0, 5, 7, 12, 7, 5],

        'rhythm': [0.0, 0.5, 0.75, 1.5, 2.25, 3.0],

        'durations': [0.4, 0.25, 0.3, 0.5, 0.3, 0.4],

        'style': 'driving'

    },

    'driving': {

        'intervals': [0, 4, 7, 4, 0, 4, 5, 7],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],

        'durations': [0.35, 0.35, 0.35, 0.35, 0.35, 0.35, 0.35, 0.4],

        'style': 'repeating'

    },

    'sparse': {

        'intervals': [0, 7],

        'rhythm': [0.0, 2.0],

        'durations': [0.4, 0.4],

        'style': 'minimal'

    },

    'minimal': {

        'intervals': [0],

        'rhythm': [0.0],

        'durations': [0.3],

        'style': 'single'

    },

    'ethereal': {

        'intervals': [0, 7, 12, 7],

        'rhythm': [0.0, 1.5, 2.5, 3.5],

        'durations': [1.0, 0.8, 1.0, 0.8],

        'style': 'pad'

    },

    'filtered': {

        'intervals': [0, 4, 7, 5],

        'rhythm': [0.0, 1.0, 2.0, 3.0],

        'durations': [0.5, 0.4, 0.5, 0.4],

        'style': 'filtered'

    },

    'fading': {

        'intervals': [0, 4, 0],

        'rhythm': [0.0, 1.0, 2.0],

        'durations': [0.5, 0.4, 0.3],

        'style': 'decay'

    },

    'echo': {

        'intervals': [0, 0, 0],

        'rhythm': [0.0, 0.5, 1.0],

        'durations': [0.3, 0.25, 0.2],

        'style': 'repeat'

    },

    'response': {

        'intervals': [7, 4, 0],

        'rhythm': [0.5, 1.5, 2.5],

        'durations': [0.3, 0.3, 0.4],

        'style': 'call_response'

    },

    'lift': {

        'intervals': [0, 4, 7, 12, 14, 16],

        'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5],

        'durations': [0.3, 0.3, 0.3, 0.4, 0.3, 0.4],

        'style': 'ascending'

    },

    'strip_down': {

        'intervals': [0],

        'rhythm': [0.0],

        'durations': [0.25],

        'style': 'minimal'

    },

    'decay': {

        'intervals': [0, 7, 5, 3],

        'rhythm': [0.0, 1.0, 2.0, 3.0],

        'durations': [0.5, 0.4, 0.3, 0.2],

        'style': 'descending'

    },

    'call_response': {

        'intervals': [0, 4, 7, 0, 7, 4],

        'rhythm': [0.0, 0.25, 0.5, 1.5, 2.0, 2.5],

        'durations': [0.25, 0.2, 0.3, 0.35, 0.25, 0.3],

        'style': 'call_response'

    },

}



# =============================================================================

# MASTER CHAIN AUTOMATION TARGETS

# =============================================================================





@dataclass

class StyleConfig:

    """ConfiguraciÃ³n de estilo musical"""

    genre: str

    bpm: float

    key: str

    scale: str

    density: str  # minimal, normal, busy

    complexity: str  # simple, moderate, complex









class SectionVariationManager:
    """
    P3: Manages sample and pattern variations across sections.
    
    Ensures:
    1. Different samples/loops per section (no repetition)
    2. Section-appropriate selection (groove, density, complexity)
    3. Tracking of used samples to prevent re-use
    
    For reggaeton/perreo Safaera-like structure:
    - Intro: More air, less density, minimal elements
    - Build: Tension, call-response, rising elements
    - Drop: Density, energy, resolution
    - Break: Relief, space, reduced density
    - Outro: Controlled degradation
    """
    
    # Section density profiles (0-1 scale)
    SECTION_DENSITY_PROFILES = {
        'intro': {'density': 0.3, 'complexity': 'low', 'air': 0.7, 'energy_target': 0.25},
        'verse': {'density': 0.5, 'complexity': 'medium', 'air': 0.5, 'energy_target': 0.45},
        'build': {'density': 0.7, 'complexity': 'high', 'air': 0.3, 'energy_target': 0.72},
        'drop': {'density': 1.0, 'complexity': 'high', 'air': 0.1, 'energy_target': 1.0},
        'break': {'density': 0.4, 'complexity': 'low', 'air': 0.6, 'energy_target': 0.38},
        'outro': {'density': 0.35, 'complexity': 'low', 'air': 0.65, 'energy_target': 0.32},
    }
    
    # Roles that get unique samples per section (avoid repetition)
    # NOTE: vocal_shot and vocal_loop removed - vocals are manual-only
    VARIATION_ROLES = {'perc_loop', 'top_loop', 'perc_alt', 'synth_peak', 
        'atmos_fx', 'fill_fx'}
    
    # Roles that stay consistent (anchor roles)
    ANCHOR_ROLES = {'kick', 'clap', 'hat_closed', 'sub_bass', 'bass'}
    
    def __init__(self, seed: int = 42):
        self.rng = random.Random(seed)
        self.used_samples_by_section: Dict[str, Set[str]] = {}
        self.section_samples: Dict[str, Dict[str, Any]] = {}
        self.variation_history: List[Dict[str, Any]] = []
        
    def get_section_profile(self, section_kind: str) -> Dict[str, Any]:
        """Get the density/complexity profile for a section type."""
        return self.SECTION_DENSITY_PROFILES.get(section_kind, 
                                                   self.SECTION_DENSITY_PROFILES['drop'])
    
    def should_use_variation(self, role: str, section_kind: str) -> bool:
        """
        Determine if a role should use a unique sample for this section.
        
        Anchor roles stay consistent, variation roles change per section.
        """
        if role in self.ANCHOR_ROLES:
            return False
        if role in self.VARIATION_ROLES:
            return True
        # Default: vary based on section density
        profile = self.get_section_profile(section_kind)
        return profile['density'] > 0.5
    
    def get_sample_complexity_preference(self, section_kind: str) -> str:
        """Get preferred sample complexity for a section."""
        profile = self.get_section_profile(section_kind)
        return profile['complexity']
    
    def score_sample_for_section(self, sample_name: str, role: str, 
                                  section_kind: str) -> float:
        """
        Score how well a sample matches a section's needs.
        
        Returns 0.7-1.4 multiplier based on:
        - Complexity match
        - Groove indicators
        - Energy level appropriateness
        """
        score = 1.0
        name_lower = sample_name.lower()
        profile = self.get_section_profile(section_kind)
        preferred_complexity = profile['complexity']
        
        # Complexity matching
        complexity_keywords = {
            'low': ['simple', 'minimal', 'basic', 'sparse', 'light', 'subtle', 'clean'],
            'medium': ['standard', 'balanced', 'normal'],
            'high': ['complex', 'rich', 'full', 'heavy', 'layered', 'busy', 'big', 'driving']
        }
        
        matches = sum(1 for kw in complexity_keywords.get(preferred_complexity, []) 
                      if kw in name_lower)
        if matches > 0:
            score += matches * 0.08  # Up to 1.24
        
        # Groove indicators (boost all sections)
        groove_keywords = ['groove', 'swing', 'shuffle', 'human', 'live', 'organic', 
                          'funk', 'soul', 'tribal', 'latin', 'dembow']
        groove_matches = sum(1 for kw in groove_keywords if kw in name_lower)
        if groove_matches > 0:
            score += groove_matches * 0.06  # Up to 1.18
        
        # Section-specific preferences
        if section_kind == 'intro':
            # Prefer subtle, airy samples
            if any(kw in name_lower for kw in ['subtle', 'minimal', 'foreshadow', 'hint', 'ghost']):
                score += 0.12
        elif section_kind == 'build':
            # Prefer rising, tension-building samples
            if any(kw in name_lower for kw in ['building', 'rising', 'tension', 'anticipate', 'energy']):
                score += 0.15
        elif section_kind == 'drop':
            # Prefer full, punchy samples
            if any(kw in name_lower for kw in ['full', 'punch', 'impact', 'heavy', 'driving']):
                score += 0.15
        elif section_kind == 'break':
            # Prefer atmospheric, reduced samples
            if any(kw in name_lower for kw in ['atmos', 'sparse', 'minimal', 'filtered', 'ethereal']):
                score += 0.12
        elif section_kind == 'outro':
            # Prefer fading, degrading samples
            if any(kw in name_lower for kw in ['fading', 'decay', 'minimal', 'strip', 'echo']):
                score += 0.12
        
        return max(0.7, min(1.4, score))
    
    def register_section_sample(self, section_kind: str, role: str, 
                                 sample_path: str, sample_name: str):
        """Track a sample used for a section/role combination."""
        if section_kind not in self.section_samples:
            self.section_samples[section_kind] = {}
        
        self.section_samples[section_kind][role] = {
            'path': sample_path,
            'name': sample_name,
            'timestamp': time.time() if 'time' in globals() else 0
        }
        
        # Add to used samples for this section
        if section_kind not in self.used_samples_by_section:
            self.used_samples_by_section[section_kind] = set()
        self.used_samples_by_section[section_kind].add(sample_path)
    
    def get_section_sample(self, section_kind: str, role: str) -> Optional[Dict[str, Any]]:
        """Get the sample registered for a section/role."""
        return self.section_samples.get(section_kind, {}).get(role)
    
    def is_sample_used_in_section(self, section_kind: str, sample_path: str) -> bool:
        """Check if a sample has been used in a section."""
        return sample_path in self.used_samples_by_section.get(section_kind, set())
    
    def get_variation_summary(self) -> Dict[str, Any]:
        """Get summary of section variations for manifest."""
        summary = {
            'total_sections': len(self.section_samples),
            'sections': {},
            'variation_roles_used': list(self.VARIATION_ROLES),
            'anchor_roles': list(self.ANCHOR_ROLES)
        }
        
        for section_kind, roles in self.section_samples.items():
            summary['sections'][section_kind] = {
                'role_count': len(roles),
                'roles': list(roles.keys()),
                'unique_samples': len(set(r['path'] for r in roles.values()))
            }
        
        return summary



class SongGenerator:

    """Generador de configuraciones y patrones musicales"""



    def __init__(self):

        self.logger = logging.getLogger("SongGenerator")

        self._current_generation_profile = {

            'name': 'default',

            'seed': 0,

            'drum_tightness': 1.0,

            'bass_motion': 'locked',

            'melodic_motion': 'restrained',

            'pan_width': 0.12,

            'fx_bias': 1.0,

        }

        # Track style adjustments and calibrated volumes for this generation

        self._style_adjustments_applied = []

        self._calibrated_bus_volumes = {}

        # Tracking for ROLE_GAIN_CALIBRATION overrides

        self._gain_calibration_overrides_count = 0

        self._peak_reductions_count = 0

        self._master_profile_used = 'default'

        # Musical theme for unified track identity (generated per-track)
        self.musical_theme: Optional[MusicalTheme] = None
        
        # MIDI hook tracking - ensures at least one MIDI harmonic track is created
        # TWO separate states - not one!
        self._hook_planned = False      # Blueprint phase - hook was planned
        self._hook_planned_data: Optional[Dict[str, Any]] = None  # Hook data from planning
        
        self._hook_materialized = False  # Ableton Live phase - actually created
        self._hook_materialized_idx: Optional[int] = None  # Actual track index in Live
        
        # Legacy compatibility - DEPRECATED, use _hook_planned and _hook_materialized
        self._midi_hook_created = False
        self._midi_hook_data: Optional[Dict[str, Any]] = None
        
        # =========================================================================
        # TRACK BUDGET ENFORCEMENT - Hard limit at 16 tracks
        # =========================================================================
        self.track_budget_max = 16  # Hard maximum tracks
        self.track_budget_count = 0  # Current track count
        self.track_budget_created = []  # List of created tracks with metadata
        self.track_budget_omitted = []  # List of roles that were skipped
        self.track_budget_priority = {
            'CORE_MANDATORY': ['sc_trigger', 'kick', 'clap', 'bass'],  # Must have
            'CORE': ['hat_closed', 'sub_bass', 'chords', 'lead', 'stab'],  # Important
            'SECONDARY': ['snare_fill', 'hat_open', 'top_loop', 'perc', 'drone', 'pad', 'arp'],  # Nice to have
            'OPTIONAL': ['tom_fill', 'ride', 'crash', 'pluck', 'counter',
                        'reverse_fx', 'riser', 'impact', 'atmos'],  # Only if budget allows (vocal removed - manual only)
        }
        self.track_budget_optional_limit = 3  # Max optional tracks
        
        # P3: Section variation manager for human feel
        self.section_variation_manager = SectionVariationManager(seed=int(time.time() * 1000) % 10000)



    def initialize_musical_theme(self, key: str = 'Am', scale: str = 'minor', seed: Optional[int] = None):
        """
        Initialize the shared musical theme for this generation.
        
        This should be called at the start of track generation to create
        the unified motif that will be used across all sections.
        
        Args:
            key: Musical key (e.g., 'Am', 'F#m', 'C')
            scale: Scale type ('minor', 'major', etc.)
            seed: Optional seed for reproducible motif generation
        """
        self.musical_theme = MusicalTheme(key=key, scale=scale, seed=seed)
        self.logger.info(f"[THEME] Initialized musical theme: key={key}, scale={scale}, seed={self.musical_theme.seed}")
        return self.musical_theme



    def _get_theme_based_notes(self, role: str, section: Dict[str, Any], total_length: float) -> Optional[List[Dict[str, Any]]]:
        """
        Get notes for a role based on the shared musical theme.
        
        If musical_theme is initialized, derive notes from the section's motif variation.
        Returns None if theme is not available.
        
        Args:
            role: Track role ('bass', 'sub_bass', 'lead', 'chords', etc.)
            section: Section configuration with 'kind' key
            total_length: Length of section in beats
            
        Returns:
            List of note dicts or None
        """
        if not self.musical_theme:
            return None
            
        kind = section.get('kind', 'drop')
        motif = self.musical_theme.get_section_variation(kind)
        
        if role in ['bass', 'sub_bass']:
            notes = self.musical_theme.motif_to_bass(motif, octave_offset=-2 if role == 'sub_bass' else -1)
            # Extend to section length
            return self._repeat_pattern(notes, total_length, 8.0)
            
        elif role == 'chords':
            chords = self.musical_theme.motif_to_chords(motif, voicing='triad')
            # Convert chord format to note format
            notes = []
            for chord in chords:
                for pitch in chord['notes']:
                    notes.append({
                        'pitch': pitch,
                        'start': chord['start'],
                        'duration': chord['duration'],
                        'velocity': chord['velocity']
                    })
            return self._repeat_pattern(notes, total_length, 8.0)
            
        elif role in ['lead', 'pluck', 'arp']:
            embellishment = 0.7 if role == 'lead' else (0.4 if role == 'arp' else 0.3)
            notes = self.musical_theme.motif_to_lead(motif, embellishment_level=embellishment)
            return self._repeat_pattern(notes, total_length, 8.0)
            
        elif role == 'counter':
            notes = self.musical_theme.motif_to_lead(motif, embellishment_level=0.2)
            # Transpose down and make sparse
            notes = [self._make_note(n['pitch'] - 12, n['start'], n['duration'], max(50, n['velocity'] - 15)) 
                     for n in notes if n['start'] % 4.0 >= 2.0]
            return self._repeat_pattern(notes, total_length, 8.0)
            
        return None



    # =========================================================================
    # TRACK BUDGET ENFORCEMENT METHODS
    # =========================================================================
    
    def _reset_track_budget(self):
        """Reset track budget counters at start of generation."""
        self.track_budget_count = 0
        self.track_budget_created = []
        self.track_budget_omitted = []
        self.logger.info(f"[BUDGET] Track budget reset: max={self.track_budget_max}, optional_limit={self.track_budget_optional_limit}")
    
    def _get_role_priority(self, role: str) -> str:
        """Get priority level for a role."""
        role_lower = role.lower().strip()
        for priority, roles in self.track_budget_priority.items():
            if role_lower in [r.lower() for r in roles]:
                return priority
        return 'OPTIONAL'  # Default to optional if not found
    
    def _can_create_track(self, role: str) -> bool:
        """
        Check if we can create a track for this role within budget.
        
        Returns True if track can be created, False if budget is exhausted.
        Logs appropriate warnings when budget limits are reached.
        """
        # Check hard limit
        if self.track_budget_count >= self.track_budget_max:
            if role not in self.track_budget_omitted:
                self.track_budget_omitted.append(role)
                self.logger.warning(f"[BUDGET_EXHAUSTED] Cannot create '{role}' - hard limit {self.track_budget_max} reached")
            return False
        
        priority = self._get_role_priority(role)
        
        # Check optional limit
        if priority == 'OPTIONAL':
            optional_count = len([t for t in self.track_budget_created if t.get('priority') == 'OPTIONAL'])
            if optional_count >= self.track_budget_optional_limit:
                if role not in self.track_budget_omitted:
                    self.track_budget_omitted.append(role)
                    self.logger.info(f"[OPTIONAL_BUDGET_FULL] Skipping '{role}' - optional limit {self.track_budget_optional_limit} reached")
                return False
        
        return True
    
    def _track_created(self, role: str, name: str, track_type: str = 'midi'):
        """Record that a track was created."""
        priority = self._get_role_priority(role)
        self.track_budget_count += 1
        track_info = {
            'order': self.track_budget_count,
            'role': role,
            'name': name,
            'type': track_type,
            'priority': priority,
        }
        self.track_budget_created.append(track_info)
        self.logger.info(f"[TRACK_CREATED] {self.track_budget_count}/{self.track_budget_max} - {name} ({role}, {priority})")
    
    def _log_budget_summary(self):
        """Log final budget summary."""
        core_mandatory = len([t for t in self.track_budget_created if t.get('priority') == 'CORE_MANDATORY'])
        core = len([t for t in self.track_budget_created if t.get('priority') == 'CORE'])
        secondary = len([t for t in self.track_budget_created if t.get('priority') == 'SECONDARY'])
        optional = len([t for t in self.track_budget_created if t.get('priority') == 'OPTIONAL'])
        
        self.logger.info(f"[BUDGET_SUMMARY] Created {self.track_budget_count}/{self.track_budget_max} tracks")
        self.logger.info(f"  - CORE_MANDATORY: {core_mandatory}")
        self.logger.info(f"  - CORE: {core}")
        self.logger.info(f"  - SECONDARY: {secondary}")
        self.logger.info(f"  - OPTIONAL: {optional}")
        if self.track_budget_omitted:
            self.logger.info(f"  - OMITTED ({len(self.track_budget_omitted)}): {', '.join(self.track_budget_omitted[:5])}{'...' if len(self.track_budget_omitted) > 5 else ''}")
    
    def _get_budget_tracking(self) -> Dict[str, Any]:
        """Get budget tracking data for manifest."""
        return {
            'max_tracks': self.track_budget_max,
            'tracks_created': self.track_budget_count,
            'budget_exceeded': self.track_budget_count > self.track_budget_max,
            'tracks_list': self.track_budget_created,
            'omitted_roles': self.track_budget_omitted,
            'core_mandatory_count': len([t for t in self.track_budget_created if t.get('priority') == 'CORE_MANDATORY']),
            'core_count': len([t for t in self.track_budget_created if t.get('priority') == 'CORE']),
            'secondary_count': len([t for t in self.track_budget_created if t.get('priority') == 'SECONDARY']),
            'optional_count': len([t for t in self.track_budget_created if t.get('priority') == 'OPTIONAL']),
        }



    # =========================================================================
    # UTILIDADES MUSICALES
    # =========================================================================



    def note_name_to_midi(self, note_name: str, octave: int = 3) -> int:

        """Convierte nombre de nota a nÃºmero MIDI"""

        note_name = note_name.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')

        note_name = note_name.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')



        try:

            note_idx = NOTE_NAMES.index(note_name.upper())

            return (octave + 1) * 12 + note_idx

        except ValueError:

            return 60  # Default C4



    def midi_to_note_name(self, midi_note: int) -> tuple:

        """Convierte MIDI a (nota, octava)"""

        octave = (midi_note // 12) - 1

        note_name = NOTE_NAMES[midi_note % 12]

        return note_name, octave



    def get_scale_notes(self, root_note: Union[int, str], scale_name: str = 'minor') -> List[int]:

        """Obtiene las notas de una escala"""

        if isinstance(root_note, str):

            root_midi = self.note_name_to_midi(root_note)

        else:

            root_midi = root_note



        scale_intervals = SCALES.get(scale_name, SCALES['minor'])

        return [root_midi + interval for interval in scale_intervals]



    def quantize_to_scale(self, note: int, scale_notes: List[int]) -> int:

        """Cuantiza una nota a la escala mÃ¡s cercana"""

        if note in scale_notes:

            return note

        return min(scale_notes, key=lambda x: abs(x - note))



    # =========================================================================

    # GENERACIÃ“N DE CONFIGURACIONES

    # =========================================================================



    def _make_note(self, pitch: int, start: float, duration: float, velocity: int) -> Dict[str, Any]:

        return {

            'pitch': max(0, min(127, int(pitch))),

            'start': round(float(start), 3),

            'duration': round(max(0.05, float(duration)), 3),

            'velocity': max(1, min(127, int(velocity))),

        }



    def _repeat_pattern(self, pattern: List[Dict[str, Any]], total_length: float, pattern_length: float = 4.0) -> List[Dict[str, Any]]:

        if not pattern or total_length <= 0 or pattern_length <= 0:

            return []



        notes = []

        repeats = max(1, int(round(total_length / pattern_length)))

        for repeat_index in range(repeats):

            offset = repeat_index * pattern_length

            for note in pattern:

                start = float(note['start']) + offset

                if start >= total_length:

                    continue

                duration = min(float(note['duration']), total_length - start)

                notes.append(self._make_note(note['pitch'], start, duration, note['velocity']))

        return notes



    def _section_rng(self, section: Dict[str, Any], role: str, salt: int = 0) -> random.Random:

        base_seed = int(self._current_generation_profile.get('seed', 0))

        section_index = int(section.get('index', 0))

        role_fingerprint = sum((index + 1) * ord(char) for index, char in enumerate(str(role)))

        return random.Random(base_seed + (section_index * 1009) + (role_fingerprint * 17) + (salt * 7919))



    def _clamp_pan(self, value: float) -> float:

        return round(max(-1.0, min(1.0, float(value))), 3)



    def _clamp_unit(self, value: float) -> float:

        return round(max(0.0, min(1.0, float(value))), 3)



    def _apply_swing(self, notes: List[Dict[str, Any]], amount: float, section_length: float) -> List[Dict[str, Any]]:

        if not notes or abs(amount) < 0.001:

            return notes



        swung = []

        for note in notes:

            start = float(note['start'])

            fractional = round(start % 1.0, 3)

            if 0.001 < fractional < 0.999:

                shift = amount if fractional >= 0.5 else (amount * -0.45)

                start = min(max(0.0, start + shift), max(0.0, section_length - 0.05))

            swung.append(self._make_note(note['pitch'], start, note['duration'], note['velocity']))

        swung.sort(key=lambda item: (item['start'], item['pitch']))

        return swung



    def _apply_density_mask(self, notes: List[Dict[str, Any]], section: Dict[str, Any], role: str,

                            keep_probability: float) -> List[Dict[str, Any]]:

        """
        Aplica mÃ¡scara de densidad a notas MIDI.
        
        IMPORTANTE (T2): Esta funciÃ³n puede crear VARIACIÃ“N POR SILENCIO si no se usa con cuidado.
        Reduce la densidad de notas eliminando notas off-beat basÃ¡ndose en probabilidad.
        
        Reglas de seguridad:
        - Nunca elimina todas las notas (siempre mantiene al menos el 30% o 2 notas)
        - Preserva notas en downbeats (mÃºltiplos de 1.0)
        - NO crea silencio largo (>4 beats) sin relleno compensatorio
        
        Args:
            notes: Lista de notas MIDI
            section: ConfiguraciÃ³n de secciÃ³n
            role: Rol del track
            keep_probability: Probabilidad de mantener cada nota (0.0-1.0)
                             
        Returns:
            Lista filtrada de notas con garantÃ­a de continuidad mÃ­nima
        """

        if not notes or keep_probability >= 0.995:

            return notes
        
        # T2: Calcular mÃ­nimo absoluto para evitar silencio total
        # Nunca permitir menos del 30% de las notas originales o menos de 2 notas
        min_notes_to_keep = max(2, int(len(notes) * 0.3))

        rng = self._section_rng(section, role, salt=3)

        filtered = []

        for note in notes:

            start = float(note['start'])

            if abs(start % 1.0) < 0.001:

                filtered.append(note)

                continue

            if rng.random() <= keep_probability:

                filtered.append(note)

        
        # T2: GarantÃ­a de continuidad - nunca retornar menos del mÃ­nimo
        if len(filtered) < min_notes_to_keep:
            # Recuperar notas eliminadas para mantener continuidad
            missing_count = min_notes_to_keep - len(filtered)
            eliminated_notes = [n for n in notes if n not in filtered]
            if eliminated_notes:
                # Elegir notas mÃ¡s importantes (las mÃ¡s cercanas a downbeats)
                eliminated_notes.sort(key=lambda n: abs(float(n['start']) % 1.0))
                filtered.extend(eliminated_notes[:missing_count])
                logger.debug("[T2_DENSITY_MASK] Recuperadas %d notas para evitar silencio excesivo en %s", 
                           missing_count, role)

        return filtered



    def _build_arrangement_profile(self, genre: str, style: str, variant_seed: int) -> Dict[str, Any]:

        style_text = "{} {}".format(genre, style).lower()

        candidates = [profile for profile in ARRANGEMENT_PROFILES if genre in set(profile.get('genres', ()))]



        if genre == 'reggaeton':

            candidates = [profile for profile in ARRANGEMENT_PROFILES if 'reggaeton' in set(profile.get('genres', ()))] or candidates

        elif 'latin' in style_text:

            candidates = [profile for profile in ARRANGEMENT_PROFILES if profile['name'] in ['swing', 'jackin']] or candidates

        elif 'industrial' in style_text:

            candidates = [profile for profile in ARRANGEMENT_PROFILES if profile['name'] in ['warehouse', 'festival']] or candidates



        if not candidates:

            candidates = list(ARRANGEMENT_PROFILES)



        rng = random.Random(int(variant_seed) + 41)

        selected = dict(rng.choice(candidates))

        selected['seed'] = int(variant_seed)

        return selected



    def _extend_parallel_sends(self, role: str, sends: Dict[str, Any]) -> Dict[str, Any]:

        resolved = dict(sends or {})

        if role in ['kick', 'clap', 'hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'snare_fill', 'tom_fill']:

            resolved.setdefault('glue', 0.1)

            resolved.setdefault('heat', 0.05)

        elif role in ['sub_bass', 'bass', 'stab']:

            resolved.setdefault('glue', 0.08)

            resolved.setdefault('heat', 0.08)

        elif role in ['chords', 'pad', 'pluck', 'arp', 'lead', 'counter', 'vocal']:

            resolved.setdefault('glue', 0.04)

        elif role in ['reverse_fx', 'riser', 'impact', 'atmos', 'drone', 'crash']:

            resolved.setdefault('glue', 0.03)

        return resolved



    def _resolve_bus_for_role(self, role: str) -> Optional[str]:

        return ROLE_BUS_ASSIGNMENTS.get(str(role or '').strip().lower(), 'music')



    def _get_section_variation(self, role: str, section_kind: str) -> Dict[str, Any]:

        """

        Obtiene configuraciÃ³n de variaciÃ³n para un rol y secciÃ³n.



        Retorna dict con:

        - use: bool - si el rol debe usarse en esta secciÃ³n

        - sparse: bool - si usar variante sparse

        - full: bool - si usar variante completa

        - intensity: float - intensidad de 0 a 1

        - etc.

        """

        if role not in SECTION_VARIATION_CONFIG:

            return {'use': True, 'intensity': 1.0}



        role_config = SECTION_VARIATION_CONFIG[role]

        return role_config.get(section_kind.lower(), {'use': True, 'intensity': 1.0})



    def _should_vary_role_in_section(self, role: str, section_kind: str) -> bool:

        """Determina si un rol debe variar en una secciÃ³n dada."""

        if role not in SECTION_VARIATION_CONFIG:

            return False



        config = self._get_section_variation(role, section_kind)



        # Si tiene clave 'use' explÃ­cita

        if 'use' in config:

            return config['use']



        # Si tiene variantes especÃ­ficas

        return any(k in config for k in ['sparse', 'full', 'building', 'fading'])



    def _build_mix_bus_blueprint(

        self,

        profile: Dict[str, Any],

        genre: str,

        style: str,

        reference_resolution: Optional[Dict[str, Any]] = None,

    ) -> List[Dict[str, Any]]:

        style_text = f"{genre} {style}".lower()

        profile_name = str(profile.get('name', 'default')).lower()

        reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()



        buses = [

            {

                'key': 'drums',

                'name': 'DRUM BUS',

                'color': BUS_TRACK_COLORS['drums'],

                'volume': 0.86,

                'pan': 0.0,

                'monitoring': 'in',

                'fx_chain': [

                    {'device': 'Compressor', 'parameters': {'Threshold': -16.5}},

                    {'device': 'Saturator', 'parameters': {'Drive': 1.2}},

                    {'device': 'Utility', 'parameters': {'Gain': 0.2}},

                    {'device': 'Limiter', 'parameters': {'Gain': 0.3}},

                ],

            },

            {

                'key': 'bass',

                'name': 'BASS BUS',

                'color': BUS_TRACK_COLORS['bass'],

                'volume': 0.8,

                'pan': 0.0,

                'monitoring': 'in',

                'fx_chain': [

                    {'device': 'Saturator', 'parameters': {'Drive': 1.3}},

                    {'device': 'Compressor', 'parameters': {'Threshold': -18.0}},

                    {'device': 'Utility', 'parameters': {'Stereo Width': 0.0}},

                    {'device': 'Utility', 'parameters': {'Gain': 0.2}},

                ],

            },

            {

                'key': 'music',

                'name': 'MUSIC BUS',

                'color': BUS_TRACK_COLORS['music'],

                'volume': 0.8,

                'pan': 0.0,

                'monitoring': 'in',

                'fx_chain': [

                    {'device': 'Compressor', 'parameters': {'Threshold': -21.0}},

                    {'device': 'Auto Filter', 'parameters': {'Frequency': 12800.0, 'Dry/Wet': 0.05}},

                    {'device': 'Utility', 'parameters': {'Stereo Width': 1.12}},

                    {'device': 'Utility', 'parameters': {'Gain': 0.2}},

                ],

            },

            {

                'key': 'vocal',

                'name': 'VOCAL BUS',

                'color': BUS_TRACK_COLORS['vocal'],

                'volume': 0.82,

                'pan': 0.0,

                'monitoring': 'in',

                'fx_chain': [

                    {'device': 'Echo', 'parameters': {'Dry/Wet': 0.05}},

                    {'device': 'Compressor', 'parameters': {'Threshold': -18.0}},

                    {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.05}},

                    {'device': 'Utility', 'parameters': {'Gain': 0.2}},

                ],

            },

            {

                'key': 'fx',

                'name': 'FX BUS',

                'color': BUS_TRACK_COLORS['fx'],

                'volume': 0.76,

                'pan': 0.0,

                'monitoring': 'in',

                'fx_chain': [

                    {'device': 'Auto Filter', 'parameters': {'Frequency': 10200.0, 'Dry/Wet': 0.1}},

                    {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}},

                    {'device': 'Utility', 'parameters': {'Gain': -0.2}},

                    {'device': 'Limiter', 'parameters': {'Gain': 0.0}},

                ],

            },

        ]



        # =========================================================================

        # Apply BUS_GAIN_CALIBRATION as safe baseline BEFORE profile overrides

        # =========================================================================

        self._style_adjustments_applied = []

        self._calibrated_bus_volumes = {}



        def find_device_in_chain(fx_chain, device_type):

            for device in fx_chain:

                if device.get('device') == device_type:

                    return device

            return None



        for bus in buses:

            bus_key = bus.get('key', '')

            if bus_key not in BUS_GAIN_CALIBRATION:

                continue



            calibration = BUS_GAIN_CALIBRATION[bus_key]



            if 'volume' in calibration:

                bus['volume'] = calibration['volume']



            fx_chain = bus.get('fx_chain', [])



            if 'compressor_threshold' in calibration:

                compressor = find_device_in_chain(fx_chain, 'Compressor')

                if compressor:

                    compressor['parameters']['Threshold'] = calibration['compressor_threshold']



            if 'saturator_drive' in calibration:

                saturator = find_device_in_chain(fx_chain, 'Saturator')

                if saturator:

                    saturator['parameters']['Drive'] = calibration['saturator_drive']



            if 'limiter_gain' in calibration:

                limiter = find_device_in_chain(fx_chain, 'Limiter')

                if limiter:

                    limiter['parameters']['Gain'] = calibration['limiter_gain']



            if 'utility_gain' in calibration:

                for device in fx_chain:

                    if device.get('device') == 'Utility':

                        if 'Gain' in device.get('parameters', {}):

                            device['parameters']['Gain'] = calibration['utility_gain']

                            break

                        elif 'Stereo Width' not in device.get('parameters', {}):

                            device['parameters']['Gain'] = calibration['utility_gain']

                            break



        # =========================================================================

        # Profile-specific overrides ON TOP of calibrated baselines

        # =========================================================================

        if profile_name == 'warehouse':

            buses[0]['name'] = 'DRUM BUNKER'

            buses[0]['fx_chain'][1]['parameters']['Drive'] = 3.1

            buses[1]['name'] = 'LOW END BUS'

            buses[1]['fx_chain'][0]['parameters']['Drive'] = 4.0

            buses[2]['fx_chain'][1]['parameters']['Frequency'] = 11200.0

        elif profile_name == 'festival':

            buses[2]['name'] = 'MUSIC WIDE'

            buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.14

            buses[3]['name'] = 'VOCAL TAIL'

            buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08

            buses[4]['name'] = 'FX WASH'

            buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.14

        elif profile_name == 'swing':

            buses[0]['name'] = 'DRUM POCKET'

            buses[0]['fx_chain'][0]['parameters']['Threshold'] = -13.5

            buses[3]['name'] = 'VOCAL SLAP'

            buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.12

        elif profile_name == 'jackin':

            buses[0]['name'] = 'DRUM CLUB'

            buses[2]['name'] = 'MUSIC JACK'

            buses[3]['name'] = 'VOX CLUB'

            buses[4]['name'] = 'FX JAM'

        elif profile_name == 'tech-house-club':

            # Club-oriented tech-house with punchy drums and latin vocal treatment

            buses[0]['name'] = 'DRUM CLUB'

            buses[0]['volume'] = 0.95

            buses[0]['fx_chain'][0]['parameters']['Threshold'] = -15.5

            buses[0]['fx_chain'][1]['parameters']['Drive'] = 2.2

            buses[1]['name'] = 'BASS TUBE'

            buses[1]['volume'] = 0.95

            buses[1]['fx_chain'][0]['parameters']['Drive'] = 2.5

            buses[1]['fx_chain'][1]['parameters']['Threshold'] = -17.0

            buses[2]['name'] = 'MUSIC JACK'

            buses[2]['volume'] = 0.95

            buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.16

            buses[3]['name'] = 'VOCAL LATIN BUS'

            buses[3]['volume'] = 0.95

            buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.10

            buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08

            buses[4]['name'] = 'FX JAM'

            buses[4]['volume'] = 0.95

            buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.14

        elif profile_name == 'tech-house-deep':

            # Minimal deep tech-house with subtle processing

            buses[0]['name'] = 'DRUM DEEP'

            buses[0]['volume'] = 0.95

            buses[0]['fx_chain'][0]['parameters']['Threshold'] = -18.0

            buses[0]['fx_chain'][1]['parameters']['Drive'] = 0.8

            buses[1]['name'] = 'SUB DEEP'

            buses[1]['volume'] = 0.95

            buses[1]['fx_chain'][0]['parameters']['Drive'] = 1.0

            buses[1]['fx_chain'][1]['parameters']['Threshold'] = -20.0

            buses[2]['name'] = 'ATMOS DEEP'

            buses[2]['volume'] = 0.95

            buses[2]['fx_chain'][0]['parameters']['Threshold'] = -24.0

            buses[2]['fx_chain'][1]['parameters']['Frequency'] = 10200.0

            buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.08

            buses[3]['name'] = 'VOX DEEP'

            buses[3]['volume'] = 0.95

            buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.04

            buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.06

            buses[4]['name'] = 'FX DEEP'

            buses[4]['volume'] = 0.95

            buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.08

        elif profile_name == 'tech-house-funky':

            # Groovy tech-house with wide stereo and bouncy feel

            buses[0]['name'] = 'DRUM GROOVE'

            buses[0]['volume'] = 0.95

            buses[0]['fx_chain'][0]['parameters']['Threshold'] = -14.5

            buses[0]['fx_chain'][1]['parameters']['Drive'] = 1.8

            buses[1]['name'] = 'BASS FUNK'

            buses[1]['volume'] = 0.95

            buses[1]['fx_chain'][0]['parameters']['Drive'] = 2.0

            buses[1]['fx_chain'][1]['parameters']['Threshold'] = -16.5

            buses[2]['name'] = 'MUSIC GROOVE'

            buses[2]['volume'] = 0.95

            buses[2]['fx_chain'][0]['parameters']['Threshold'] = -20.0

            buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.20

            buses[3]['name'] = 'VOCAL FUNK'

            buses[3]['volume'] = 0.95

            buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.12

            buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.10

            buses[4]['name'] = 'FX SWING'

            buses[4]['volume'] = 0.95

            buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.16



        if 'industrial' in style_text:

            buses[0]['fx_chain'][1]['parameters']['Drive'] = max(

                3.4,

                float(buses[0]['fx_chain'][1]['parameters'].get('Drive', 2.2)),

            )

            buses[1]['fx_chain'][0]['parameters']['Drive'] = max(

                4.2,

                float(buses[1]['fx_chain'][0]['parameters'].get('Drive', 3.2)),

            )

        if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃ­mica', 'quimica']):

            buses[3]['name'] = 'VOCAL LATIN BUS'

            buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.14

            buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08

            buses[0]['fx_chain'][0]['parameters']['Threshold'] = -14.0



        # =========================================================================

        # Apply STYLE_GAIN_ADJUSTMENTS as multipliers AFTER profile overrides

        # =========================================================================

        for style_key, adjustments in STYLE_GAIN_ADJUSTMENTS.items():

            if style_key.lower() in style_text:

                self._style_adjustments_applied.append(style_key)



                # Apply bus volume factors

                if 'drums_bus_volume_factor' in adjustments:

                    for bus in buses:

                        if bus.get('key') == 'drums':

                            bus['volume'] = bus.get('volume', 0.8) * adjustments['drums_bus_volume_factor']



                if 'bass_bus_volume_factor' in adjustments:

                    for bus in buses:

                        if bus.get('key') == 'bass':

                            bus['volume'] = bus.get('volume', 0.8) * adjustments['bass_bus_volume_factor']



                if 'vocal_bus_volume_factor' in adjustments:

                    for bus in buses:

                        if bus.get('key') == 'vocal':

                            bus['volume'] = bus.get('volume', 0.8) * adjustments['vocal_bus_volume_factor']



                if 'music_bus_volume_factor' in adjustments:

                    for bus in buses:

                        if bus.get('key') == 'music':

                            bus['volume'] = bus.get('volume', 0.8) * adjustments['music_bus_volume_factor']



                if 'fx_bus_volume_factor' in adjustments:

                    for bus in buses:

                        if bus.get('key') == 'fx':

                            bus['volume'] = bus.get('volume', 0.8) * adjustments['fx_bus_volume_factor']



                # Apply saturator_drive_factor to all bus saturators

                if 'saturator_drive_factor' in adjustments:

                    for bus in buses:

                        fx_chain = bus.get('fx_chain', [])

                        saturator = find_device_in_chain(fx_chain, 'Saturator')

                        if saturator and 'Drive' in saturator.get('parameters', {}):

                            saturator['parameters']['Drive'] = (

                                saturator['parameters']['Drive'] * adjustments['saturator_drive_factor']

                            )



                # Apply limiter_gain_factor to all bus limiters

                if 'limiter_gain_factor' in adjustments:

                    for bus in buses:

                        fx_chain = bus.get('fx_chain', [])

                        limiter = find_device_in_chain(fx_chain, 'Limiter')

                        if limiter and 'Gain' in limiter.get('parameters', {}):

                            limiter['parameters']['Gain'] = (

                                limiter['parameters']['Gain'] * adjustments['limiter_gain_factor']

                            )



        # Store final calibrated bus volumes

        for bus in buses:

            bus_key = bus.get('key', '')

            if bus_key:

                self._calibrated_bus_volumes[bus_key] = bus.get('volume', 0.0)



        # RCA Fix: Automatic Makeup and Output gain compensation

        for bus in buses:

            for device in bus.get('fx_chain', []):

                device_type = device.get('device')

                params = device.get('parameters', {})

                if device_type == 'Compressor' and 'Threshold' in params:

                    params['Makeup'] = round(abs(params['Threshold']) * 0.25, 1)

                elif device_type == 'Saturator' and 'Drive' in params:

                    params['Output'] = round(-params['Drive'] * 1.5, 1)



        return buses



    def _build_return_blueprint(

        self,

        profile: Dict[str, Any],

        genre: str,

        style: str,

        reference_resolution: Optional[Dict[str, Any]] = None,

    ) -> List[Dict[str, Any]]:

        style_text = f"{genre} {style}".lower()

        profile_name = str(profile.get('name', 'default')).lower()

        reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()

        returns = [

            {

                'name': 'MCP SPACE',

                'send_key': 'space',

                'color': 56,

                'device_chain': [{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}}],

                'volume': 0.76,

            },

            {

                'name': 'MCP ECHO',

                'send_key': 'echo',

                'color': 44,

                'device_chain': [{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}}],

                'volume': 0.72,

            },

            {

                'name': 'MCP HEAT',

                'send_key': 'heat',

                'color': 12,

                'device_chain': [

                    {'device': 'Saturator', 'parameters': {'Drive': 4.5}},

                    {'device': 'Compressor', 'parameters': {'Threshold': -16.0}},

                ],

                'volume': 0.62,

            },

            {

                'name': 'MCP GLUE',

                'send_key': 'glue',

                'color': 58,

                'device_chain': [

                    {'device': 'Compressor', 'parameters': {'Threshold': -18.0}},

                    {'device': 'Limiter', 'parameters': {'Gain': 0.0}},

                ],

                'volume': 0.68,

            },

        ]



        if profile_name == 'warehouse':

            returns[0]['name'] = 'MCP BUNKER'

            returns[0]['device_chain'] = [

                {'device': 'Auto Filter', 'parameters': {'Frequency': 7200.0, 'Dry/Wet': 0.22}},

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}},

            ]

            returns[1]['name'] = 'MCP DUB'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 8200.0, 'Dry/Wet': 0.14}},

            ]

            returns[2]['device_chain'][0]['parameters']['Drive'] = 5.5

            returns[2]['volume'] = 0.66

        elif profile_name == 'festival':

            returns[0]['name'] = 'MCP WIDE'

            returns[0]['device_chain'] = [

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}},

                {'device': 'Utility', 'parameters': {'Stereo Width': 1.14}},

            ]

            returns[1]['name'] = 'MCP TAIL'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.18}},

            ]

            returns[0]['volume'] = 0.72

            returns[1]['volume'] = 0.68

        elif profile_name == 'swing':

            returns[0]['name'] = 'MCP ROOM'

            returns[1]['name'] = 'MCP SLAP'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 9800.0, 'Dry/Wet': 0.1}},

            ]

            returns[2]['volume'] = 0.58

        elif profile_name == 'jackin':

            returns[0]['name'] = 'MCP CLUB'

            returns[1]['name'] = 'MCP SWING'

            returns[2]['device_chain'][0]['parameters']['Drive'] = 3.8

            returns[3]['volume'] = 0.72

        elif profile_name == 'tech-house-club':

            # Short reverb, mono delay, wide FX for club tech-house

            returns[0]['name'] = 'REVERB SHORT'

            returns[0]['device_chain'] = [

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 0.6}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 8400.0, 'Dry/Wet': 0.08}},

            ]

            returns[0]['volume'] = 0.70

            returns[1]['name'] = 'DELAY MONO'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Ping Pong': 0.0}},

                {'device': 'Utility', 'parameters': {'Width': 0.0}},

            ]

            returns[1]['volume'] = 0.68

            returns[2]['name'] = 'DRIVE HOT'

            returns[2]['device_chain'][0]['parameters']['Drive'] = 4.0

            returns[2]['volume'] = 0.64

            returns[3]['name'] = 'GLUE BUS'

            returns[3]['device_chain'][0]['parameters']['Threshold'] = -16.5

            returns[3]['volume'] = 0.70

        elif profile_name == 'tech-house-deep':

            # Deep minimal returns with subtle processing

            returns[0]['name'] = 'REVERB DEEP'

            returns[0]['device_chain'] = [

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 1.2}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 6200.0, 'Dry/Wet': 0.12}},

            ]

            returns[0]['volume'] = 0.72

            returns[1]['name'] = 'DELAY DEEP'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Feedback': 0.45}},

            ]

            returns[1]['volume'] = 0.64

            returns[2]['name'] = 'SATURATE DEEP'

            returns[2]['device_chain'][0]['parameters']['Drive'] = 2.5

            returns[2]['volume'] = 0.56

            returns[3]['name'] = 'GLUE MINIMAL'

            returns[3]['device_chain'][0]['parameters']['Threshold'] = -20.0

            returns[3]['volume'] = 0.62

        elif profile_name == 'tech-house-funky':

            # Groovy returns with modulation and swing

            returns[0]['name'] = 'REVERB GROOVE'

            returns[0]['device_chain'] = [

                {'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 0.8}},

                {'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.08}},

            ]

            returns[0]['volume'] = 0.74

            returns[1]['name'] = 'DELAY GROOVE'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Ping Pong': 0.4, 'Feedback': 0.35}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 8000.0, 'Dry/Wet': 0.1}},

            ]

            returns[1]['volume'] = 0.70

            returns[2]['name'] = 'DRIVE FUNK'

            returns[2]['device_chain'][0]['parameters']['Drive'] = 3.2

            returns[2]['device_chain'].append({'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.06}})

            returns[2]['volume'] = 0.60

            returns[3]['name'] = 'GLUE SWING'

            returns[3]['device_chain'][0]['parameters']['Threshold'] = -15.5

            returns[3]['volume'] = 0.72



        if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃ­mica', 'quimica']):

            returns[1]['name'] = 'MCP VOX ECHO'

            returns[1]['device_chain'] = [

                {'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},

                {'device': 'Auto Filter', 'parameters': {'Frequency': 10800.0, 'Dry/Wet': 0.12}},

            ]

            returns[0]['volume'] = max(0.68, float(returns[0]['volume']) - 0.04)

        if 'industrial' in style_text:

            returns[2]['name'] = 'MCP DRIVE'

            returns[2]['device_chain'][0]['parameters']['Drive'] = max(

                4.8,

                float(returns[2]['device_chain'][0]['parameters'].get('Drive', 4.5))

            )

            returns[3]['name'] = 'MCP BUS'



        return returns



    def _build_master_blueprint(

        self,

        profile: Dict[str, Any],

        genre: str,

        style: str,

        reference_resolution: Optional[Dict[str, Any]] = None,

    ) -> Dict[str, Any]:

        style_text = f"{genre} {style}".lower()

        profile_name = str(profile.get('name', 'default')).lower()

        reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()



        # Start with default calibration values

        calibration = dict(MASTER_CALIBRATION.get('default', {}))



        # Find matching profile (case-insensitive, partial match)

        matched_profile = 'default'

        profile_name_lower = profile_name.lower()

        for cal_key in MASTER_CALIBRATION.keys():

            if cal_key.lower() in profile_name_lower or profile_name_lower in cal_key.lower():

                # Merge profile-specific values over defaults

                profile_cal = MASTER_CALIBRATION[cal_key]

                calibration.update(profile_cal)

                matched_profile = cal_key

                break



        # Track which profile was used

        self._master_profile_used = matched_profile



        # Build master with calibrated values

        # Master chain: Utility (gain staging) -> Saturator (color) -> Compressor (glue) -> Limiter (ceiling)

        # Target: -1dB peak before limiter, -0.3dBFS ceiling after limiter

        master = {

            'volume': calibration.get('volume', 0.85),

            'device_chain': [

                {

                    'device': 'Utility',

                    'parameters': {

                        'Gain': calibration.get('utility_gain', -0.5),

                        'Stereo Width': calibration.get('stereo_width', 1.04),

                    }

                },

                {

                    'device': 'Saturator',

                    'parameters': {'Drive': calibration.get('saturator_drive', 0.12)}

                },

                {

                    'device': 'Compressor',

                    'parameters': {

                        'Ratio': calibration.get('compressor_ratio', 0.50),

                        'Attack': calibration.get('compressor_attack', 0.30),

                        'Release': calibration.get('compressor_release', 0.20),

                    }

                },

                {

                    'device': 'Limiter',

                    'parameters': {

                        'Gain': calibration.get('limiter_gain', 0.8),

                        'Ceiling': calibration.get('limiter_ceiling', -0.3),

                    }

                },

],

    # P1 Sprint v0.1.23: Reggaeton/perreo_specificstructures
    'reggaeton': [
        ('INTRO', 8, 12, 'intro', 1),
        ('GROOVE A', 16, 16, 'build', 2),
        ('DROP A', 16, 28, 'drop', 4),
        ('CORO', 8, 22, 'break', 1),
        ('GROOVE B', 8, 18, 'build', 3),
        ('DROP B', 16, 30, 'drop', 5),
        ('OUTRO', 8, 8, 'outro', 1),
    ],

    'perreo_duro': [
        ('INTRO DJ', 16, 10, 'intro', 1),
        ('DEM BOW A', 16, 14, 'build', 2),
        ('PERREO A', 16, 28, 'drop', 4),
        ('PUENTE', 8, 24, 'break', 1),
        ('DEM BOW B', 8, 18, 'build', 3),
        ('PERREO B', 16, 31, 'drop', 5),
        ('OUTRO DJ', 16, 8, 'outro', 1),
    ],

    'safaera_style': [
        ('INTRO', 4, 12, 'intro', 1),
        ('BUILD A', 8, 16, 'build', 2),
        ('DROP A', 16, 28, 'drop', 4),
        ('MID BREAK', 4, 20, 'break', 1),
        ('DROP B', 16, 30, 'drop', 5),
        ('PUENTE', 8, 22, 'break', 2),
        ('DROP C', 16, 32, 'drop', 6),
        ('OUTRO', 8, 10, 'outro', 1),
    ],

}



        # Apply style-based limiter_gain_factor from STYLE_GAIN_ADJUSTMENTS

        for style_key, style_adj in STYLE_GAIN_ADJUSTMENTS.items():

            if style_key.lower() in style_text:

                limiter_factor = style_adj.get('limiter_gain_factor')

                if limiter_factor is not None:

                    master['device_chain'][3]['parameters']['Gain'] *= limiter_factor

                break



        if 'industrial' in style_text:

            master['device_chain'][1]['parameters']['Drive'] = max(

                0.8,

                float(master['device_chain'][1]['parameters'].get('Drive', 0.3))

            )

            master['device_chain'][2]['parameters']['Ratio'] = max(

                0.7,

                float(master['device_chain'][2]['parameters'].get('Ratio', 0.62))

            )



        if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃ­mica', 'quimica']):

            master['device_chain'][0]['parameters']['Stereo Width'] = max(

                1.14,

                float(master['device_chain'][0]['parameters'].get('Stereo Width', 1.1))

            )

            master['device_chain'][3]['parameters']['Gain'] = max(

                0.1,

                float(master['device_chain'][3]['parameters'].get('Gain', 0.0))

            )



        return master



    def _apply_role_gain_calibration(self, role: str, base_volume: float) -> Dict[str, float]:

        """

        Apply ROLE_GAIN_CALIBRATION to a role's volume.



        Args:

            role: The role name (e.g., 'kick', 'bass', 'clap')

            base_volume: The base volume from ROLE_MIX



        Returns:

            Dict with 'volume' and optionally 'saturator_drive' if calibrated

        """

        if role not in ROLE_GAIN_CALIBRATION:

            return {'volume': base_volume}



        calibration = ROLE_GAIN_CALIBRATION[role]

        calibrated_volume = float(calibration.get('volume', base_volume))



        # Apply peak_reduction if present

        peak_reduction = calibration.get('peak_reduction', 0.0)

        if peak_reduction > 0:

            calibrated_volume *= (1.0 - float(peak_reduction))

            self._peak_reductions_count += 1



        result = {'volume': round(max(0.0, min(1.0, calibrated_volume)), 3)}



        # Include saturator_drive if present in calibration

        if 'saturator_drive' in calibration:

            result['saturator_drive'] = float(calibration['saturator_drive'])



        self._gain_calibration_overrides_count += 1



        return result



    def _shape_mix_profile(self, role: str, mix_profile: Dict[str, Any], profile: Dict[str, Any], style: str) -> Dict[str, Any]:

        shaped = {

            'volume': float(mix_profile.get('volume', 0.72)),

            'pan': float(mix_profile.get('pan', 0.0)),

            'sends': dict(mix_profile.get('sends', {})),

        }



        # Apply ROLE_GAIN_CALIBRATION if available - overrides base volume

        calibration = self._apply_role_gain_calibration(role, shaped['volume'])

        if calibration.get('volume') is not None:

            shaped['volume'] = calibration['volume']

        if calibration.get('saturator_drive') is not None:

            shaped['saturator_drive'] = calibration['saturator_drive']



        profile_name = str(profile.get('name', 'default')).lower()

        pan_width = float(profile.get('pan_width', 0.16) or 0.16)

        style_text = str(style or '').lower()



        if role in ['hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'pluck', 'arp', 'counter', 'vocal']:

            shaped['pan'] = max(-1.0, min(1.0, shaped['pan'] * (1.0 + pan_width)))



        if profile_name == 'warehouse':

            if role in ['kick', 'bass', 'sub_bass']:

                shaped['volume'] *= 1.03

            if role in ['pad', 'drone', 'atmos']:

                shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 0.88

            if role in ['reverse_fx', 'riser', 'impact']:

                shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.08)

        elif profile_name == 'festival':

            if role in ['lead', 'chords', 'pad', 'arp', 'vocal']:

                shaped['volume'] *= 1.04

                shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.15

            if role in ['kick', 'clap']:

                shaped['sends']['glue'] = max(shaped['sends'].get('glue', 0.0), 0.12)

        elif profile_name == 'swing':

            if role in ['perc', 'top_loop', 'ride', 'vocal', 'pluck']:

                shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.14

            if role in ['kick', 'sub_bass']:

                shaped['volume'] *= 0.98

        elif profile_name == 'jackin':

            if role in ['clap', 'perc', 'vocal', 'counter']:

                shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.08

            if role in ['top_loop', 'ride']:

                shaped['volume'] *= 1.03

        elif profile_name == 'tech-house-club':

            # Club-oriented: punchy drums, present vocals, tight bass

            if role in ['kick', 'clap']:

                shaped['volume'] *= 1.02

                shaped['sends']['glue'] = max(shaped['sends'].get('glue', 0.0), 0.10)

            if role in ['bass', 'sub_bass']:

                shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.06)

            if role in ['vocal', 'counter']:

                shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.10

            if role in ['hat_open', 'top_loop', 'ride']:

                shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 0.92

        elif profile_name == 'tech-house-deep':

            # Deep minimal: subtle processing, wide stereo

            if role in ['kick', 'sub_bass']:

                shaped['volume'] *= 0.98

            if role in ['pad', 'drone', 'atmos', 'chords']:

                shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.12

            if role in ['perc', 'top_loop']:

                shaped['volume'] *= 0.95

                shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 0.88

        elif profile_name == 'tech-house-funky':

            # Funky groove: wider pan, more echo, bouncy feel

            if role in ['perc', 'top_loop', 'ride']:

                shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.18

            if role in ['bass', 'sub_bass']:

                shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.05)

            if role in ['vocal', 'pluck', 'arp']:

                shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.08

            if role in ['clap', 'hat_closed']:

                shaped['volume'] *= 1.02



        if 'latin' in style_text and role in ['perc', 'top_loop', 'ride', 'vocal']:

            shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.12

            shaped['pan'] = max(-1.0, min(1.0, shaped['pan'] * 1.08))

        if 'industrial' in style_text and role in ['kick', 'bass', 'stab', 'impact', 'riser']:

            shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.09)



        shaped['volume'] = round(max(0.0, min(1.0, shaped['volume'])), 3)

        shaped['pan'] = round(max(-1.0, min(1.0, shaped['pan'])), 3)

        shaped['sends'] = {

            send_key: round(max(0.0, min(1.0, float(send_value))), 3)

            for send_key, send_value in shaped['sends'].items()

        }

        return shaped



    def _shape_role_fx_chain(self, role: str, profile: Dict[str, Any], style: str) -> List[Dict[str, Any]]:

        chain = [dict(item) for item in ROLE_FX_CHAINS.get(role, [])]

        profile_name = str(profile.get('name', 'default')).lower()

        style_text = str(style or '').lower()



        if profile_name == 'warehouse':

            if role in ['kick', 'bass', 'stab']:

                chain.append({'device': 'Compressor', 'parameters': {'Threshold': -18.0}})

            if role in ['atmos', 'drone', 'pad']:

                chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 7600.0, 'Dry/Wet': 0.14}})

        elif profile_name == 'festival':

            if role in ['lead', 'arp', 'vocal']:

                chain.append({'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.1}})

            if role in ['chords', 'pad']:

                chain.append({'device': 'Utility', 'parameters': {'Width': 140.0}})

        elif profile_name == 'swing':

            if role in ['perc', 'top_loop', 'ride', 'vocal']:

                chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}})

        elif profile_name == 'jackin':

            if role in ['clap', 'perc', 'vocal', 'counter']:

                chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.5}})

        elif profile_name == 'tech-house-club':

            # Club: punchy drums, saturated bass, crisp tops

            if role in ['kick', 'clap']:

                chain.append({'device': 'Compressor', 'parameters': {'Threshold': -16.0, 'Attack': 0.02}})

            if role in ['bass', 'sub_bass']:

                chain.append({'device': 'Saturator', 'parameters': {'Drive': 2.0}})

            if role in ['hat_closed', 'hat_open', 'top_loop']:

                chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 12000.0, 'Dry/Wet': 0.12}})

            if role in ['vocal', 'counter']:

                chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}})

        elif profile_name == 'tech-house-deep':

            # Deep: subtle saturation, atmospheric processing

            if role in ['kick', 'bass']:

                chain.append({'device': 'Compressor', 'parameters': {'Threshold': -20.0}})

            if role in ['pad', 'drone', 'atmos']:

                chain.append({'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}})

            if role in ['chords', 'pluck']:

                chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 9200.0, 'Dry/Wet': 0.08}})

        elif profile_name == 'tech-house-funky':

            # Funky: groove-enhancing FX, modulation

            if role in ['perc', 'top_loop', 'ride']:

                chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.10, 'Ping Pong': 0.3}})

            if role in ['bass', 'sub_bass']:

                chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.8}})

            if role in ['vocal', 'pluck', 'arp']:

                chain.append({'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.06}})

            if role in ['clap', 'hat_closed']:

                chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.2}})



        if 'industrial' in style_text and role in ['kick', 'bass', 'impact', 'riser']:

            chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.8}})

        if 'latin' in style_text and role in ['perc', 'top_loop', 'ride', 'vocal']:

            chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 11200.0, 'Dry/Wet': 0.1}})



        return chain



    def _get_section_drum_variant(self, role: str, section: Dict[str, Any]) -> str:

        """Get appropriate drum variant for section and role with cross-generation diversity."""

        kind = str(section.get('kind', 'drop')).lower()

        role_lower = role.lower()



        if role_lower not in DRUM_SECTION_VARIANTS.get(kind, {}):

            return 'straight'



        variants = list(DRUM_SECTION_VARIANTS[kind][role_lower])

        valid_variants = [v for v in variants if v in DRUM_PATTERN_BANKS.get(role_lower, {})]

        if not valid_variants and role_lower in DRUM_PATTERN_BANKS:

            valid_variants = list(DRUM_PATTERN_BANKS[role_lower].keys())



        if not valid_variants:

            return 'straight'



        rng = self._section_rng(section, role, salt=1)



        if len(valid_variants) > 1:

            scored_variants = []

            for v in valid_variants:

                penalty = _get_pattern_variant_penalty('drum', f'{role_lower}_{v}')

                score = rng.random() - penalty

                scored_variants.append((score, v))

            scored_variants.sort(reverse=True)

            chosen = scored_variants[0][1]

        else:

            chosen = valid_variants[0]



        _record_pattern_variant_usage('drum', f'{role_lower}_{chosen}')

        return chosen



    def _generate_drum_pattern_from_bank(self, role: str, variant: str,

                                          section_length: float,

                                          velocity_base: int = 100) -> List[Dict[str, Any]]:

        """Generate drum pattern from pattern bank."""

        role_lower = role.lower()



        if role_lower not in DRUM_PATTERN_BANKS:

            return []



        bank = DRUM_PATTERN_BANKS[role_lower]

        if variant not in bank:

            variant = list(bank.keys())[0]  # Fallback to first



        positions = bank[variant]

        notes = []



        # Determine pitch based on role

        pitch_map = {

            'kick': 36, 'clap': 39, 'hat_closed': 42,

            'hat_open': 46, 'perc': 50, 'ride': 51

        }

        pitch = pitch_map.get(role_lower, 36)



        for pos in positions:

            # Repeat pattern for each bar

            for bar in range(int(section_length // 4)):

                start = pos + (bar * 4.0)

                if start < section_length:

                    # Add slight velocity variation

                    velocity = max(60, min(127, velocity_base + random.randint(-10, 10)))

                    duration = 0.1 if role_lower in ['hat_closed', 'hat_open', 'ride'] else 0.15

                    notes.append(self._make_note(pitch, start, duration, velocity))



        logger.debug(f"Generated drum pattern from bank: role={role}, variant={variant}, notes={len(notes)}")

        return notes



    def _get_section_bass_variant(self, section: Dict[str, Any]) -> str:

        """Get appropriate bass variant for section with cross-generation diversity."""

        kind = str(section.get('kind', 'drop')).lower()



        if kind not in BASS_SECTION_VARIANTS:

            return 'anchor'



        variants = list(BASS_SECTION_VARIANTS[kind])

        valid_variants = [v for v in variants if v in BASS_PATTERN_BANKS]

        if not valid_variants:

            valid_variants = list(BASS_PATTERN_BANKS.keys())



        rng = self._section_rng(section, 'bass', salt=2)



        if len(valid_variants) > 1:

            scored_variants = []

            for v in valid_variants:

                penalty = _get_pattern_variant_penalty('bass', v)

                score = rng.random() - penalty

                scored_variants.append((score, v))

            scored_variants.sort(reverse=True)

            chosen = scored_variants[0][1]

        else:

            chosen = valid_variants[0] if valid_variants else 'anchor'



        _record_pattern_variant_usage('bass', chosen)

        return chosen



    def _compute_section_signature(self, section: Dict[str, Any]) -> str:

        """Compute a signature for section to detect repetition."""

        section = self._ensure_section_pattern_variants(section)

        signature_parts = []

        drum_role_variants = dict(section.get('drum_role_variants') or {})



        signature_parts.append(f"kick:{drum_role_variants.get('kick', section.get('drum_variant', 'default'))}")

        signature_parts.append(f"clap:{drum_role_variants.get('clap', section.get('drum_variant', 'default'))}")

        signature_parts.append(f"hat:{drum_role_variants.get('hat_closed', section.get('drum_variant', 'default'))}")

        signature_parts.append(f"bass:{section.get('bass_bank_variant', section.get('bass_variant', 'default'))}")

        signature_parts.append(f"lead:{section.get('melodic_bank_variant', section.get('melodic_variant', 'default'))}")

        signature_parts.append(f"fill:{section.get('transition_fill', 'none')}")



        # Add density and swing

        density = section.get('density', 1.0)

        swing = section.get('swing', 0.0)

        signature_parts.append(f"d:{density:.1f}")

        signature_parts.append(f"s:{swing:.2f}")



        return "|".join(signature_parts)



    def _check_section_repetition(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:

        """Check and warn about excessive section repetition."""

        signatures = []

        consecutive_same = 0

        max_consecutive = 2



        for i, section in enumerate(sections):

            self._ensure_section_pattern_variants(section)

            sig = self._compute_section_signature(section)



            if signatures and signatures[-1] == sig:

                consecutive_same += 1

                if consecutive_same >= max_consecutive:

                    logger.warning("REPETITION: %d consecutive sections with same signature: %s",

                                 consecutive_same + 1, sig)

                    self._force_section_pattern_variation(section)

                    sig = self._compute_section_signature(section)

            else:

                consecutive_same = 0



            signatures.append(sig)



        return sections



    def _record_section_variant(self, section: Dict[str, Any], role: str, variant: str):

        """Record variant used for a role in a section."""

        key = f'{role}_variant'

        section[key] = variant



    def _choose_alternate_variant(self, options: List[str], current: Optional[str], rng: random.Random) -> Optional[str]:

        ordered: List[str] = []

        for option in options:

            if option not in ordered:

                ordered.append(option)

        if not ordered:

            return current

        alternatives = [option for option in ordered if option != current]

        if not alternatives:

            return current or ordered[0]

        return rng.choice(alternatives)



    def _ensure_section_pattern_variants(self, section: Dict[str, Any]) -> Dict[str, Any]:

        _kind = str(section.get('kind', 'drop')).lower()  # noqa: F841 - used by helper methods via section dict

        drum_role_variants = dict(section.get('drum_role_variants') or {})

        for role in ['kick', 'clap', 'hat_closed', 'hat_open', 'perc', 'ride']:

            if role in drum_role_variants:

                continue

            variant = self._get_section_drum_variant(role, section)

            if variant in DRUM_PATTERN_BANKS.get(role, {}):

                drum_role_variants[role] = variant

                self._record_section_variant(section, role, variant)

        section['drum_role_variants'] = drum_role_variants



        bass_bank_variant = str(section.get('bass_bank_variant', '') or '')

        if bass_bank_variant not in BASS_PATTERN_BANKS:

            bass_bank_variant = self._get_section_bass_variant(section)

            section['bass_bank_variant'] = bass_bank_variant

        self._record_section_variant(section, 'bass_bank', str(section.get('bass_bank_variant', 'anchor')))



        melodic_bank_variant = str(section.get('melodic_bank_variant', '') or '')

        if melodic_bank_variant not in MELODIC_PATTERN_BANKS:

            melodic_bank_variant = self._get_section_melodic_variant(section)

            section['melodic_bank_variant'] = melodic_bank_variant

        self._record_section_variant(section, 'melodic_bank', str(section.get('melodic_bank_variant', 'motif')))

        section.setdefault('pattern_variant_ready', True)

        return section



    def _force_section_pattern_variation(self, section: Dict[str, Any]) -> Dict[str, Any]:

        kind = str(section.get('kind', 'drop')).lower()

        self._ensure_section_pattern_variants(section)

        drum_role_variants = dict(section.get('drum_role_variants') or {})



        for role in ['kick', 'clap', 'hat_closed']:

            options = DRUM_SECTION_VARIANTS.get(kind, {}).get(role, [])

            current = drum_role_variants.get(role)

            next_variant = self._choose_alternate_variant(options, current, self._section_rng(section, role, salt=101))

            if next_variant:

                drum_role_variants[role] = next_variant

                self._record_section_variant(section, role, next_variant)

        section['drum_role_variants'] = drum_role_variants



        bass_options = BASS_SECTION_VARIANTS.get(kind, [])

        bass_variant = self._choose_alternate_variant(

            bass_options,

            str(section.get('bass_bank_variant', '') or ''),

            self._section_rng(section, 'bass', salt=102),

        )

        if bass_variant:

            section['bass_bank_variant'] = bass_variant

            self._record_section_variant(section, 'bass_bank', bass_variant)



        melodic_options = MELODIC_SECTION_VARIANTS.get(kind, [])

        melodic_variant = self._choose_alternate_variant(

            melodic_options,

            str(section.get('melodic_bank_variant', '') or ''),

            self._section_rng(section, 'melodic', salt=103),

        )

        if melodic_variant:

            section['melodic_bank_variant'] = melodic_variant

            self._record_section_variant(section, 'melodic_bank', melodic_variant)



        return section



    def _generate_bass_pattern_from_bank(self, variant: str, key: str,

                                          section_length: float,

                                          velocity_base: int = 95) -> List[Dict[str, Any]]:

        """Generate bass pattern from pattern bank."""

        if variant not in BASS_PATTERN_BANKS:

            variant = 'anchor'



        bank = BASS_PATTERN_BANKS[variant]

        positions = bank['positions']

        durations = bank['durations']

        style = bank.get('style', 'root')



        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, 2)



        notes = []

        for bar in range(int(section_length // 4)):

            for i, pos in enumerate(positions):

                start = pos + (bar * 4.0)

                if start < section_length:

                    duration = durations[i] if i < len(durations) else 0.4

                    velocity = max(70, min(120, velocity_base + random.randint(-8, 8)))



                    # Adjust pitch based on style

                    pitch = root_midi

                    if style == 'ascending' and bar > 0:

                        pitch += min(bar, 5)  # Rise over bars

                    elif style == 'syncopated' and i % 2 == 1:

                        pitch += 5  # Fifth on offbeats



                    notes.append(self._make_note(pitch, start, duration, velocity))



        logger.debug(f"Generated bass pattern from bank: variant={variant}, notes={len(notes)}")

        return notes



    def _vary_drum_notes(self, notes: List[Dict[str, Any]], role: str, section: Dict[str, Any],

                         section_length: float) -> List[Dict[str, Any]]:

        section = self._ensure_section_pattern_variants(section)

        role_variant = str((section.get('drum_role_variants') or {}).get(role, '') or '').lower()

        kind = str(section.get('kind', 'drop')).lower()

        density = float(section.get('density', 1.0))

        _ = int(section.get('energy', 1))

        variant = str(section.get('drum_variant', 'straight')).lower()

        swing = float(section.get('swing', 0.0))

        tightness = float(self._current_generation_profile.get('drum_tightness', 1.0))

        rng = self._section_rng(section, role, salt=5)



        if role_variant in DRUM_PATTERN_BANKS.get(role, {}):

            logger.debug(f"Using section pattern bank for {role} with variant {role_variant} in section {kind}")

            bank_notes = self._generate_drum_pattern_from_bank(role, role_variant, section_length)

            if bank_notes:

                use_bank_prob = 0.85 if kind in ['intro', 'break', 'outro'] else 0.95

                if rng.random() < use_bank_prob or not notes:

                    return bank_notes



        if not notes:

            if role in DRUM_PATTERN_BANKS:

                all_variants = list(DRUM_PATTERN_BANKS[role].keys())

                if all_variants:

                    fallback_variant = rng.choice(all_variants)

                    return self._generate_drum_pattern_from_bank(role, fallback_variant, section_length)

            return []



        varied = list(notes)



        if variant == 'skip' and role in ['hat_closed', 'hat_open', 'top_loop', 'perc', 'ride']:

            varied = self._apply_density_mask(varied, section, role, keep_probability=min(0.94, max(0.54, density - 0.08)))

        elif variant == 'pressure' and role in ['kick', 'hat_closed', 'perc']:

            pressure_notes = []

            for bar_start in range(0, int(section_length), 4):

                if role == 'kick' and rng.random() > 0.35:

                    pressure_notes.append(self._make_note(36, min(section_length - 0.05, bar_start + 3.5), 0.12, 92))

                elif role == 'hat_closed' and rng.random() > 0.45:

                    pressure_notes.append(self._make_note(42, min(section_length - 0.05, bar_start + 3.75), 0.06, 58))

                elif role == 'perc' and rng.random() > 0.5:

                    pressure_notes.append(self._make_note(50, min(section_length - 0.05, bar_start + 3.25), 0.12, 74))

            varied = self._merge_section_notes(varied, pressure_notes, section_length)

        elif variant == 'shuffle' and role not in ['kick', 'clap', 'sc_trigger', 'crash']:

            varied = self._apply_swing(varied, swing or (0.035 / max(0.8, tightness)), section_length)



        if swing > 0.0 and role in ['top_loop', 'perc', 'ride']:

            varied = self._apply_swing(varied, swing * 0.55, section_length)



        return varied



    def _vary_bass_notes(self, notes: List[Dict[str, Any]], role: str, key: str,

                         section: Dict[str, Any], section_length: float) -> List[Dict[str, Any]]:

        section = self._ensure_section_pattern_variants(section)

        bank_variant = str(section.get('bass_bank_variant', '') or '').lower()

        kind = str(section.get('kind', 'drop')).lower()

        variant = str(section.get('bass_variant', 'anchor')).lower()



        if bank_variant in BASS_PATTERN_BANKS:

            logger.debug(f"Using section bass pattern bank for variant {bank_variant} in section {kind}")

            return self._generate_bass_pattern_from_bank(bank_variant, key, section_length)



        if not notes:

            if bank_variant in BASS_PATTERN_BANKS:

                return self._generate_bass_pattern_from_bank(bank_variant, key, section_length)

            all_variants = list(BASS_PATTERN_BANKS.keys())

            if all_variants:

                rng = self._section_rng(section, role, salt=7)

                fallback = rng.choice(all_variants)

                return self._generate_bass_pattern_from_bank(fallback, key, section_length)

            return []



        profile_motion = str(self._current_generation_profile.get('bass_motion', 'locked')).lower()

        rng = self._section_rng(section, role, salt=7)

        root_note = key[:-1] if len(key) > 1 else key

        scale_name = 'minor' if 'm' in key.lower() else 'major'

        root_midi = self.note_name_to_midi(root_note, 2)

        scale_notes = self.get_scale_notes(root_midi, scale_name)



        varied = []

        for index, note in enumerate(notes):

            pitch = int(note['pitch'])

            start = float(note['start'])

            duration = float(note['duration'])

            velocity = int(note['velocity'])



            if variant == 'anchor' and (start % 4.0) < 0.001:

                pitch = root_midi

                duration = max(duration, 0.5)

            elif variant == 'bounce' and (start % 1.0) >= 0.5:

                velocity = min(124, velocity + 8)

                duration = max(0.18, duration * 0.82)

            elif variant == 'syncopated' and (start % 1.0) < 0.001 and rng.random() > 0.4:

                start = min(section_length - 0.05, start + 0.25)

                duration = max(0.16, duration * 0.68)

            elif variant == 'pedal' and index % 3 == 0:

                pitch = root_midi



            if profile_motion == 'lifted' and index % 8 == 6:

                pitch += 12

            elif profile_motion == 'syncopated' and rng.random() > 0.72:

                pitch = scale_notes[(index + 4) % len(scale_notes)]

            elif profile_motion == 'bouncy' and (start % 4.0) >= 2.0:

                velocity = min(124, velocity + 5)



            varied.append(self._make_note(pitch, start, duration, velocity))



        return self._shape_notes_for_section(varied, kind, role, section_length)



    def _get_section_melodic_variant(self, section: Dict[str, Any]) -> str:

        """Get appropriate melodic variant for section with cross-generation diversity."""

        kind = str(section.get('kind', 'drop')).lower()



        if kind not in MELODIC_SECTION_VARIANTS:

            return 'motif'



        variants = list(MELODIC_SECTION_VARIANTS[kind])

        valid_variants = [v for v in variants if v in MELODIC_PATTERN_BANKS]

        if not valid_variants:

            valid_variants = list(MELODIC_PATTERN_BANKS.keys())



        rng = self._section_rng(section, 'melodic', salt=3)



        if len(valid_variants) > 1:

            scored_variants = []

            for v in valid_variants:

                penalty = _get_pattern_variant_penalty('melodic', v)

                score = rng.random() - penalty

                scored_variants.append((score, v))

            scored_variants.sort(reverse=True)

            chosen = scored_variants[0][1]

        else:

            chosen = valid_variants[0] if valid_variants else 'motif'



        _record_pattern_variant_usage('melodic', chosen)

        return chosen



    def _generate_melodic_pattern_from_bank(self, variant: str, key: str,

                                             scale_name: str,

                                             section_length: float,

                                             velocity_base: int = 90) -> List[Dict[str, Any]]:

        """Generate melodic pattern from pattern bank."""

        if variant not in MELODIC_PATTERN_BANKS:

            variant = 'motif'



        bank = MELODIC_PATTERN_BANKS[variant]

        intervals = bank['intervals']

        rhythm = bank['rhythm']

        durations = bank['durations']



        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, 5)

        scale_notes = self.get_scale_notes(root_midi, scale_name)



        notes = []

        for bar in range(int(section_length // 4)):

            for i, pos in enumerate(rhythm):

                start = pos + (bar * 4.0)

                if start < section_length:

                    interval = intervals[i] if i < len(intervals) else intervals[-1]

                    pitch = scale_notes[interval % len(scale_notes)]

                    duration = durations[i] if i < len(durations) else 0.3

                    velocity = max(60, min(110, velocity_base + random.randint(-10, 10)))



                    notes.append(self._make_note(pitch, start, duration, velocity))



        logger.debug(f"Generated melodic pattern from bank: variant={variant}, notes={len(notes)}")

        return notes



    def _vary_melodic_notes(self, notes: List[Dict[str, Any]], role: str, key: str, scale_name: str,

                            section: Dict[str, Any], section_length: float) -> List[Dict[str, Any]]:

        section = self._ensure_section_pattern_variants(section)

        bank_variant = str(section.get('melodic_bank_variant', '') or '').lower()

        kind = str(section.get('kind', 'drop')).lower()



        if bank_variant in MELODIC_PATTERN_BANKS:

            logger.debug(f"Using section melodic pattern bank for variant {bank_variant} in section {kind}")

            return self._generate_melodic_pattern_from_bank(bank_variant, key, scale_name, section_length)



        if not notes:

            if bank_variant in MELODIC_PATTERN_BANKS:

                return self._generate_melodic_pattern_from_bank(bank_variant, key, scale_name, section_length)

            all_variants = list(MELODIC_PATTERN_BANKS.keys())

            if all_variants:

                rng = self._section_rng(section, role, salt=11)

                fallback = rng.choice(all_variants)

                return self._generate_melodic_pattern_from_bank(fallback, key, scale_name, section_length)

            return []



        variant = str(section.get('melodic_variant', 'motif')).lower()

        profile_motion = str(self._current_generation_profile.get('melodic_motion', 'restrained')).lower()

        rng = self._section_rng(section, role, salt=11)

        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, 5)

        scale_notes = self.get_scale_notes(root_midi, scale_name)



        transformed = []

        for index, note in enumerate(notes):

            start = float(note['start'])

            pitch = int(note['pitch'])

            duration = float(note['duration'])

            velocity = int(note['velocity'])

            keep = True



            if variant == 'response' and int(start / 2.0) % 2 == 0 and role in ['lead', 'pluck', 'counter']:

                keep = False

            elif variant == 'lift' and index % 4 == 3:

                pitch += 12

                velocity = min(124, velocity + 10)

            elif variant == 'descend' and index % 5 == 4:

                pitch -= 12

                duration = max(0.16, duration * 0.9)

            elif variant == 'drone':

                keep = (start % 4.0) < 0.001 or duration >= 0.5

                if keep:

                    pitch = scale_notes[index % min(3, len(scale_notes))]

                    duration = max(duration, 1.2)



            if keep and profile_motion in ['anthemic', 'hooky'] and role in ['lead', 'arp', 'pluck']:

                if rng.random() > 0.78:

                    pitch += 12

                elif profile_motion == 'hooky' and rng.random() > 0.84:

                    start = min(section_length - 0.05, start + 0.25)



            if keep and profile_motion == 'call_response' and role in ['counter', 'pluck'] and (start % 4.0) < 2.0:

                velocity = max(52, velocity - 8)



            if keep:

                transformed.append(self._make_note(pitch, start, duration, velocity))



        if role in ['arp', 'pluck'] and float(section.get('swing', 0.0)) > 0.0:

            transformed = self._apply_swing(transformed, float(section.get('swing', 0.0)) * 0.45, section_length)



        return self._shape_notes_for_section(transformed, kind, role, section_length)



    def _transpose_notes(self, notes: List[Dict[str, Any]], semitones: int) -> List[Dict[str, Any]]:

        return [

            self._make_note(note['pitch'] + semitones, note['start'], note['duration'], note['velocity'])

            for note in notes

        ]



    def _scale_note_lengths(self, notes: List[Dict[str, Any]], factor: float, minimum: float = 0.1) -> List[Dict[str, Any]]:

        scaled = []

        for note in notes:

            scaled.append(

                self._make_note(

                    note['pitch'],

                    note['start'],

                    max(minimum, float(note['duration']) * factor),

                    note['velocity'],

                )

            )

        return scaled



    def _shape_notes_for_section(self, notes: List[Dict[str, Any]], section_kind: str, role: str,

                                 section_length: float) -> List[Dict[str, Any]]:

        if not notes:

            return []



        shaped = []

        for note in notes:

            start = float(note['start'])

            keep = True



            if section_kind in ['intro', 'outro'] and role in ['bass', 'sub_bass', 'lead', 'pluck', 'arp', 'counter']:

                keep = int(start * 2) % 4 == 0

            elif section_kind == 'break' and role in ['bass', 'sub_bass', 'lead', 'pluck', 'arp', 'counter', 'clap', 'hat_open', 'ride']:

                keep = int(start) % 4 == 0



            if keep and start < section_length:

                duration = min(float(note['duration']), section_length - start)

                shaped.append(self._make_note(note['pitch'], start, duration, note['velocity']))

        return shaped



    def _merge_section_notes(self, base_notes: List[Dict[str, Any]], extra_notes: List[Dict[str, Any]],

                             section_length: float) -> List[Dict[str, Any]]:

        merged = []

        for note in list(base_notes) + list(extra_notes):

            start = float(note['start'])

            if start >= section_length:

                continue

            duration = min(float(note['duration']), max(0.05, section_length - start))

            merged.append(self._make_note(note['pitch'], start, duration, note['velocity']))

        merged.sort(key=lambda item: (item['start'], item['pitch']))

        return merged



    def _build_drum_fill(self, role: str, section_length: float, intensity: int) -> List[Dict[str, Any]]:

        fill_start = max(0.0, section_length - 1.0)

        if role == 'kick' and intensity >= 3:

            return [self._make_note(36, fill_start + step, 0.14, 112 + (idx % 2) * 8) for idx, step in enumerate([0.0, 0.25, 0.5, 0.75])]

        if role == 'clap' and intensity >= 3:

            return [self._make_note(39, fill_start + step, 0.18, 92 + idx * 6) for idx, step in enumerate([0.25, 0.5, 0.75])]

        if role == 'hat_closed':

            return [self._make_note(42, fill_start + (idx * 0.125), 0.06, 64 + (idx % 4) * 6) for idx in range(8)]

        if role == 'perc' and intensity >= 2:

            return [

                self._make_note(37, fill_start + 0.125, 0.08, 72),

                self._make_note(47, fill_start + 0.375, 0.08, 76),

                self._make_note(50, fill_start + 0.625, 0.1, 82),

            ]

        return []



    def _build_turnaround_notes(self, key: str, scale_name: str, section_length: float,

                                octave: int, velocity: int = 92) -> List[Dict[str, Any]]:

        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, octave)

        scale_notes = self.get_scale_notes(root_midi, scale_name)

        fill_start = max(0.0, section_length - 2.0)

        degrees = [0, 2, 4, 6]

        notes = []

        for index, degree in enumerate(degrees):

            pitch = scale_notes[degree % len(scale_notes)]

            notes.append(self._make_note(pitch, fill_start + (index * 0.5), 0.38, velocity + index * 4))

        return notes



    def _generate_fill_pattern(self, fill_name: str, start_offset: float) -> Tuple[List[Dict[str, Any]], List[str]]:

        """

        Generate fill pattern at specified offset.



        Returns:

            (notes, roles) - tuple of note list and list of roles used

        """

        if fill_name not in FILL_PATTERNS:

            return [], []



        fill = FILL_PATTERNS[fill_name]

        notes = []

        roles_used = []



        pitch_map = {

            'kick': 36, 'snare': 38, 'hat': 42, 'hat_open': 46,

            'crash': 49, 'ride': 51, 'perc': 50

        }



        for role, positions in fill['pattern'].items():

            roles_used.append(role)

            pitch = pitch_map.get(role, 50)

            velocity = fill['velocities'].get(role, 90)



            for pos in positions:

                start = start_offset + pos

                duration = 0.1 if role in ['hat', 'hat_open', 'ride'] else 0.15

                notes.append(self._make_note(pitch, start, duration, velocity))



        # Track materialization for debugging/logging

        if not hasattr(self, '_transition_materialization_log'):

            self._transition_materialization_log = []

        self._transition_materialization_log.append({

            'fill': fill_name,

            'start': start_offset,

            'notes_count': len(notes),

            'roles': roles_used

        })



        return notes, roles_used



    def _generate_transition_events(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:

        """Generate fill and transition events between sections."""

        transition_events = []



        # Calculate start positions for each section

        arrangement_time = 0.0

        for section in sections:

            section['start'] = arrangement_time

            arrangement_time += float(section.get('beats', 0.0) or 0.0)



        for i, section in enumerate(sections):

            kind = str(section.get('kind', '')).lower()

            start = float(section.get('start', 0.0))

            length = float(section.get('beats', 8.0))

            end = start + length



            # Check for transition to next section

            if i < len(sections) - 1:

                next_kind = str(sections[i + 1].get('kind', '')).lower()

                transition_key = (kind, next_kind)



                if transition_key in TRANSITION_EVENTS:

                    fills = TRANSITION_EVENTS[transition_key]

                    rng = self._section_rng(section, 'transition', salt=20)

                    fill_name = rng.choice(fills)



                    # Get notes and roles from fill pattern

                    fill_notes, fill_roles = self._generate_fill_pattern(fill_name, end - 2.0)



                    transition_events.append({

                        'fill': fill_name,

                        'start': end - 2.0,

                        'section_kind': kind,

                        'next_section_kind': next_kind,

                        'roles': fill_roles,

                        'notes': fill_notes,  # Include actual notes for materialization

                        'notes_count': len(fill_notes)

                    })

                    logger.debug("TRANSITION: Added '%s' at %.1f for %s->%s",

                               fill_name, end - 2.0, kind, next_kind)



        return transition_events



    def _apply_transition_density_rules(self, transition_events: List[Dict],

                                         sections: List[Dict]) -> List[Dict]:

        """

        Apply anti-overcrowding rules to transition events.



        Returns filtered list of events.

        """

        if not transition_events:

            return []



        filtered = []

        last_event_time = {}  # Track last time of each event type

        section_fill_counts = defaultdict(int)  # Track fills per section



        for event in transition_events:

            fill_name = event.get('fill', '')

            start = event.get('start', 0.0)

            section_kind = event.get('section_kind', 'drop')



            # Rule 1: Max fills per section

            max_fills = TRANSITION_DENSITY_RULES['max_fills_by_section'].get(section_kind, 2)

            if section_fill_counts[section_kind] >= max_fills:

                logger.debug("TRANSITION_DENSITY: Skipping '%s' - section '%s' at max (%d fills)",

                            fill_name, section_kind, max_fills)

                continue



            # Rule 2: Minimum distance between same-type events

            min_dist = TRANSITION_DENSITY_RULES['min_distance_same_type'].get(fill_name, 0)

            if fill_name in last_event_time:

                time_since_last = start - last_event_time[fill_name]

                if time_since_last < min_dist:

                    logger.debug("TRANSITION_DENSITY: Skipping '%s' - too close to previous (%.1f < %.1f)",

                                fill_name, time_since_last, min_dist)

                    continue



            # Rule 3: Check for exclusive events at same position

            skip = False

            for existing in filtered:

                if abs(existing.get('start', -999) - start) < 0.5:  # Same position

                    for exclusive_set in TRANSITION_DENSITY_RULES['exclusive_events']:

                        if fill_name in exclusive_set and existing.get('fill') in exclusive_set:

                            logger.debug("TRANSITION_DENSITY: Skipping '%s' - exclusive with '%s' at %.1f",

                                        fill_name, existing.get('fill'), start)

                            skip = True

                            break

                if skip:

                    break



            if skip:

                continue



            # Event passes all rules

            filtered.append(event)

            last_event_time[fill_name] = start

            section_fill_counts[section_kind] += 1



        logger.info("TRANSITION_DENSITY: %d events passed filtering (from %d original)",

                    len(filtered), len(transition_events))



        return filtered



    def _transition_events_to_notes(self, transition_events: List[Dict]) -> List[Dict]:

        """Convert filtered transition events to MIDI notes."""

        notes = []

        for event in transition_events:

            fill_name = event.get('fill', '')

            start = event.get('start', 0.0)

            fill_notes, _ = self._generate_fill_pattern(fill_name, start)

            notes.extend(fill_notes)

        return notes



    def _materialize_transition_events(self, config: Dict[str, Any],

                                        track_blueprints: List[Dict]) -> List[Dict]:

        """

        Materialize transition events into track blueprints.



        Adds actual MIDI notes to transition-oriented tracks based on transition_events config.

        """

        transition_events = config.get('transition_events', [])

        if not transition_events:

            config['transition_materialization'] = {

                'events_count': 0,

                'materialized': False,

                'note_count': 0,

                'track_roles': [],

            }

            return track_blueprints



        transition_track_targets = {

            'drum_fill_4bar': 'snare_fill',

            'drum_fill_2bar': 'snare_fill',

            'snare_roll': 'snare_fill',

            'hat_open_build': 'riser',

            'kick_drop': 'impact',

            'crash_impact': 'crash',

        }

        pitch_to_track_role = {

            36: 'kick',

            38: 'snare_fill',

            42: 'hat_closed',

            46: 'hat_open',

            49: 'crash',

            50: 'perc',

            51: 'ride',

        }



        # Build a lookup dict of tracks by role

        tracks_by_role = {}

        for track in track_blueprints:

            role = track.get('role', '')

            if role:

                tracks_by_role[role] = track



        # Track what was materialized

        materialized_count = 0

        materialized_track_roles: set = set()



        # Materialize each transition event

        for event in transition_events:

            fill_name = event.get('fill', '')

            fill_start = event.get('start', 0.0)

            fill_notes = event.get('notes', [])



            if not fill_notes:

                event['materialized'] = False

                event['materialized_notes_count'] = 0

                event['materialized_track_roles'] = []

                continue



            preferred_track_role = transition_track_targets.get(fill_name)

            preferred_note_map: Dict[str, List[Dict[str, Any]]] = {}

            if preferred_track_role and preferred_track_role in tracks_by_role:

                preferred_note_map[preferred_track_role] = list(fill_notes)



            fallback_note_map: Dict[str, List[Dict[str, Any]]] = {}

            for note in fill_notes:

                note_role = pitch_to_track_role.get(int(note.get('pitch', 0)))

                if note_role:

                    fallback_note_map.setdefault(note_role, []).append(note)



            # Add notes to appropriate tracks

            event_materialized_count = 0

            event_track_roles: set = set()



            for notes_by_track_role in [preferred_note_map, fallback_note_map]:

                if not notes_by_track_role:

                    continue



                for track_role, notes_to_add in notes_by_track_role.items():

                    if track_role not in tracks_by_role:

                        logger.debug("TRANSITION_MATERIALIZATION: No track for role '%s', skipping %d notes",

                                    track_role, len(notes_to_add))

                        continue

                    if track_role in event_track_roles:

                        continue



                    track = tracks_by_role[track_role]

                    clips = track.get('clips', [])



                    for clip in clips:

                        clip_scene_index = clip.get('scene_index', -1)

                        sections = config.get('sections', [])

                        if clip_scene_index < 0 or clip_scene_index >= len(sections):

                            continue



                        section = sections[clip_scene_index]

                        section_start = float(section.get('start', 0.0))

                        section_beats = float(section.get('beats', 0.0))



                        if section_start <= fill_start < section_start + section_beats:

                            existing_notes = clip.get('notes', [])

                            adjusted_notes = []

                            for note in notes_to_add:

                                adjusted_note = dict(note)

                                adjusted_note['start'] = note['start'] - section_start

                                adjusted_notes.append(adjusted_note)



                            existing_notes.extend(adjusted_notes)

                            existing_notes.sort(key=lambda item: (float(item.get('start', 0.0)), int(item.get('pitch', 0))))

                            clip['notes'] = existing_notes

                            materialized_count += len(adjusted_notes)

                            event_materialized_count += len(adjusted_notes)

                            materialized_track_roles.add(track_role)

                            event_track_roles.add(track_role)



                            logger.debug("TRANSITION_MATERIALIZATION: Added %d notes to track '%s' (role: %s) for fill '%s' at %.1f",

                                        len(adjusted_notes), track.get('name', ''), track_role, fill_name, fill_start)

                            break



                if event_materialized_count > 0:

                    break



            event['materialized'] = event_materialized_count > 0

            event['materialized_notes_count'] = event_materialized_count

            event['materialized_track_roles'] = sorted(event_track_roles)

        logger.info("TRANSITION_MATERIALIZATION: Total %d notes materialized across all tracks", materialized_count)
        
        logger.info("[COHERENCE] materialized_track_roles=%s", sorted(materialized_track_roles))
        
        config['transition_materialization'] = {

            'events_count': len(transition_events),

            'materialized': materialized_count > 0,

            'note_count': materialized_count,

            'track_roles': sorted(materialized_track_roles),

        }

        return track_blueprints



    def _find_reference_track_profile(self) -> Optional[Dict[str, Any]]:

        matches: List[Tuple[float, Dict[str, Any]]] = []

        audio_extensions = {'.wav', '.mp3', '.aiff', '.flac', '.aif', '.ogg'}

        for directory in REFERENCE_SEARCH_DIRS:

            if not directory.exists():

                continue

            for candidate in sorted(directory.glob('*')):

                if not candidate.is_file():

                    continue

                if candidate.suffix.lower() not in audio_extensions:

                    continue

                normalized_name = candidate.name.lower()

                for profile in REFERENCE_TRACK_PROFILES:

                    if all(term in normalized_name for term in profile.get('match_terms', [])):

                        resolved = dict(profile)

                        resolved['path'] = str(candidate)

                        resolved['file_name'] = candidate.name

                        try:

                            modified = float(candidate.stat().st_mtime)

                        except Exception:

                            modified = 0.0

                        matches.append((modified, resolved))



        if not matches:

            return None

        matches.sort(key=lambda item: item[0], reverse=True)

        return matches[0][1]



    def _resolve_reference_track_profile(self, genre: str, style: str, bpm: float,

                                         key: str, structure: str,

                                         reference_energy_profile: Optional[List[Dict[str, Any]]] = None) -> Optional[Dict[str, Any]]:

        profile = self._find_reference_track_profile()

        if not profile:

            return None



        target_genre = profile.get('genre', '')

        compatible_genres = {target_genre, 'techno', 'tech-house', 'house'}

        if genre and genre not in compatible_genres:

            return None



        if bpm <= 0:

            bpm = float(profile.get('bpm', bpm or 0))

        if not key:

            key = profile.get('key', key)

        if not style:

            style = profile.get('style', style)

        if not structure or structure == 'standard':

            structure = profile.get('structure', structure or 'standard')



        result = {

            'genre': target_genre or genre,

            'style': style,

            'bpm': bpm,

            'key': key,

            'structure': structure,

            'reference': profile,

        }



        # Forward energy profile if available

        if reference_energy_profile:

            result['reference_energy_profile'] = reference_energy_profile



        return result



    def _build_return_states(self, returns: List[Dict[str, Any]], section: Dict[str, Any]) -> List[Dict[str, Any]]:

        if not returns:

            return []



        kind = str(section.get('kind', 'drop')).lower()

        energy = max(1, int(section.get('energy', 1)))

        profile_name = str(self._current_generation_profile.get('name', 'default')).lower()

        style_text = str(self._current_generation_profile.get('style_text', '')).lower()



        volume_factors = {

            'space': {'intro': 0.94, 'build': 0.84, 'drop': 0.7, 'break': 1.02, 'outro': 0.86},

            'echo': {'intro': 0.8, 'build': 1.04, 'drop': 0.72, 'break': 0.92, 'outro': 0.78},

            'heat': {'intro': 0.56, 'build': 0.88, 'drop': 1.06, 'break': 0.42, 'outro': 0.66},

            'glue': {'intro': 0.72, 'build': 0.86, 'drop': 1.02, 'break': 0.58, 'outro': 0.74},

        }

        space_mix = {'intro': 0.94, 'build': 0.88, 'drop': 0.8, 'break': 1.0, 'outro': 0.9}

        echo_mix = {'intro': 0.72, 'build': 0.92, 'drop': 0.62, 'break': 0.84, 'outro': 0.76}

        width_targets = {'intro': 1.02, 'build': 1.08, 'drop': 1.12, 'break': 1.16, 'outro': 1.04}

        filter_factors = {'intro': 0.86, 'build': 1.0, 'drop': 1.18, 'break': 0.78, 'outro': 0.9}

        drive_offsets = {'intro': -1.2, 'build': 0.2, 'drop': 1.0, 'break': -1.6, 'outro': -0.5}

        threshold_offsets = {'intro': 1.5, 'build': -0.5, 'drop': -2.0, 'break': 2.5, 'outro': 1.0}



        states = []

        for return_index, return_spec in enumerate(returns):

            send_key = str(return_spec.get('send_key', return_spec.get('name', ''))).strip().lower()

            if not send_key:

                continue



            base_volume = float(return_spec.get('volume', 0.7))

            volume_factor = volume_factors.get(send_key, {}).get(kind, 1.0)

            if send_key in ['heat', 'glue'] and energy >= 4:

                volume_factor += 0.04

            elif send_key in ['space', 'echo'] and kind == 'break':

                volume_factor += 0.04



            if profile_name == 'warehouse' and send_key == 'heat':

                volume_factor += 0.05

            elif profile_name == 'festival' and send_key == 'space':

                volume_factor += 0.06

            elif profile_name == 'swing' and send_key == 'echo':

                volume_factor += 0.05

            elif profile_name == 'jackin' and send_key == 'glue':

                volume_factor += 0.05



            if 'industrial' in style_text and send_key == 'heat':

                volume_factor += 0.05

            if 'latin' in style_text and send_key == 'echo':

                volume_factor += 0.06



            state = {

                'return_index': return_index,

                'send_key': send_key,

                'volume': self._clamp_unit(base_volume * volume_factor),

                'device_parameters': [],

            }



            for device_index, device_spec in enumerate(return_spec.get('device_chain', []) or []):

                if not isinstance(device_spec, dict):

                    continue

                device_name = str(device_spec.get('device', '') or '').strip()

                if not device_name:

                    continue

                device_name_lower = device_name.lower()

                base_parameters = dict(device_spec.get('parameters', {}))

                parameter_updates = {}



                if send_key == 'space':

                    if 'hybrid reverb' in device_name_lower:

                        parameter_updates['Dry/Wet'] = space_mix.get(kind, 0.9)

                    elif 'auto filter' in device_name_lower:

                        base_frequency = float(base_parameters.get('Frequency', 8200.0) or 8200.0)

                        parameter_updates['Frequency'] = round(base_frequency * filter_factors.get(kind, 1.0), 3)

                        parameter_updates['Dry/Wet'] = {'intro': 0.18, 'build': 0.22, 'drop': 0.08, 'break': 0.28, 'outro': 0.14}.get(kind, 0.16)

                    elif 'utility' in device_name_lower:

                        parameter_updates['Stereo Width'] = width_targets.get(kind, 1.08)

                elif send_key == 'echo':

                    if 'echo' in device_name_lower:

                        parameter_updates['Dry/Wet'] = echo_mix.get(kind, 0.78)

                    elif 'auto filter' in device_name_lower:

                        base_frequency = float(base_parameters.get('Frequency', 9800.0) or 9800.0)

                        parameter_updates['Frequency'] = round(base_frequency * {'intro': 0.94, 'build': 1.08, 'drop': 0.88, 'break': 0.9, 'outro': 0.92}.get(kind, 1.0), 3)

                        parameter_updates['Dry/Wet'] = {'intro': 0.08, 'build': 0.14, 'drop': 0.06, 'break': 0.16, 'outro': 0.09}.get(kind, 0.1)

                    elif 'hybrid reverb' in device_name_lower:

                        parameter_updates['Dry/Wet'] = {'intro': 0.12, 'build': 0.18, 'drop': 0.08, 'break': 0.22, 'outro': 0.1}.get(kind, 0.12)

                elif send_key == 'heat':

                    if 'saturator' in device_name_lower:

                        base_drive = float(base_parameters.get('Drive', 4.5) or 4.5)

                        parameter_updates['Drive'] = round(max(0.5, base_drive + drive_offsets.get(kind, 0.0)), 3)

                    elif 'compressor' in device_name_lower:

                        base_threshold = float(base_parameters.get('Threshold', -16.0) or -16.0)

                        parameter_updates['Threshold'] = round(base_threshold + threshold_offsets.get(kind, 0.0), 3)

                elif send_key == 'glue':

                    if 'compressor' in device_name_lower:

                        base_threshold = float(base_parameters.get('Threshold', -18.0) or -18.0)

                        parameter_updates['Threshold'] = round(base_threshold + {'intro': 1.0, 'build': -0.6, 'drop': -1.4, 'break': 1.8, 'outro': 0.8}.get(kind, 0.0), 3)

                    elif 'limiter' in device_name_lower:

                        parameter_updates['Gain'] = {'intro': -0.4, 'build': 0.0, 'drop': 0.35, 'break': -0.6, 'outro': -0.3}.get(kind, 0.0)



                for parameter_name, value in parameter_updates.items():

                    state['device_parameters'].append({

                        'device_index': int(device_index),

                        'device_name': device_name,

                        'parameter': parameter_name,

                        'value': value,

                    })



            states.append(state)



        return states



# =========================================================================

    # SECTION AUTOMATION METHODS

    # =========================================================================



    def _generate_automation_envelope(

        self,

        parameter_start: float,

        parameter_end: float,

        section_length: float,

        curve_name: str = 'linear',

        num_points: int = 8

    ) -> List[Dict[str, Any]]:

        """

        Generate automation envelope points for a parameter over a section.



        Args:

            parameter_start: Starting value of the parameter

            parameter_end: Ending value of the parameter

            section_length: Length of the section in beats

            curve_name: Name of the envelope curve to use

            num_points: Number of envelope points to generate



        Returns:

            List of automation points with time and value

        """

        curve_func = ENVELOPE_CURVES.get(curve_name, ENVELOPE_CURVES['linear'])

        envelope_points = []



        for i in range(num_points):

            position = i / (num_points - 1) if num_points > 1 else 0.0

            curved_position = curve_func(position)

            value = parameter_start + (parameter_end - parameter_start) * curved_position

            time = section_length * position



            envelope_points.append({

                'time': round(time, 3),

                'value': round(value, 4),

                'curve_position': round(position, 3),

            })



        return envelope_points



    def _build_section_automation(

        self,

        section: Dict[str, Any],

        buses: List[Dict[str, Any]],

        returns: List[Dict[str, Any]]

    ) -> Dict[str, Any]:

        """

        Build automation data for a single section.



        Args:

            section: Section configuration dictionary

            buses: List of bus track configurations

            returns: List of return track configurations



        Returns:

            Dictionary containing automation data for the section

        """

        kind = str(section.get('kind', 'drop')).lower()

        section_length = float(section.get('beats', 32.0))

        energy = float(section.get('energy', 1))



        # Get base automation template for this section kind

        base_automation = SECTION_AUTOMATION.get(kind, SECTION_AUTOMATION.get('drop', {}))



        # Determine envelope curve

        curve_name = base_automation.get('envelope_curve', 'linear')



        # Apply energy scaling

        energy_factor = max(0.5, min(1.5, energy / 3.0))



        automation_data = {

            'section_index': int(section.get('index', 0)),

            'section_name': section.get('name', 'SECTION'),

            'section_kind': kind,

            'section_length': section_length,

            'energy': round(base_automation.get('energy', 0.5) * energy_factor, 3),

            'bus_automation': [],

            'return_automation': [],

            'master_automation': {},

        }



        # Build bus automation

        for bus in buses:

            bus_key = str(bus.get('key', '')).lower()

            if not bus_key:

                continue



            bus_filter_settings = base_automation.get('filters', {}).get(bus_key, {})

            if not bus_filter_settings:

                continue



            bus_auto = {

                'bus_key': bus_key,

                'bus_name': bus.get('name', bus_key.upper()),

                'parameters': []

            }



            # Filter frequency automation

            if 'frequency' in bus_filter_settings:

                freq_start = bus_filter_settings['frequency'] * (1.1 - energy_factor * 0.2)

                freq_end = bus_filter_settings['frequency'] * energy_factor

                bus_auto['parameters'].append({

                    'device': 'Auto Filter',

                    'parameter': 'Frequency',

                    'envelope': self._generate_automation_envelope(

                        freq_start, freq_end, section_length, curve_name

                    ),

                    'start_value': round(freq_start, 1),

                    'end_value': round(freq_end, 1),

                })



            # Filter resonance automation

            if 'resonance' in bus_filter_settings:

                res_start = bus_filter_settings['resonance'] * 0.8

                res_end = bus_filter_settings['resonance'] * energy_factor

                bus_auto['parameters'].append({

                    'device': 'Auto Filter',

                    'parameter': 'Resonance',

                    'envelope': self._generate_automation_envelope(

                        res_start, res_end, section_length, 'ease_in_out'

                    ),

                    'start_value': round(res_start, 3),

                    'end_value': round(res_end, 3),

                })



            if bus_auto['parameters']:

                automation_data['bus_automation'].append(bus_auto)



        # Build return automation

        reverb_settings = base_automation.get('reverb', {})

        delay_settings = base_automation.get('delay', {})

        compression_settings = base_automation.get('compression', {})

        saturation_settings = base_automation.get('saturation', {})

        stereo_width_settings = base_automation.get('stereo_width', {})



        for return_track in returns:

            send_key = str(return_track.get('send_key', '')).lower()

            if not send_key:

                continue



            return_auto = {

                'send_key': send_key,

                'return_name': return_track.get('name', send_key.upper()),

                'parameters': []

            }



            if send_key == 'space' and reverb_settings:

                # Reverb send level

                return_auto['parameters'].append({

                    'device': 'Hybrid Reverb',

                    'parameter': 'Dry/Wet',

                    'envelope': self._generate_automation_envelope(

                        reverb_settings.get('send_level', 0.2) * 0.9,

                        reverb_settings.get('send_level', 0.2) * energy_factor,

                        section_length, curve_name

                    ),

                    'start_value': round(reverb_settings.get('send_level', 0.2) * 0.9, 3),

                    'end_value': round(reverb_settings.get('send_level', 0.2) * energy_factor, 3),

                })

                # Decay time

                return_auto['parameters'].append({

                    'device': 'Hybrid Reverb',

                    'parameter': 'Decay Time',

                    'envelope': self._generate_automation_envelope(

                        reverb_settings.get('decay_time', 2.0) * 0.85,

                        reverb_settings.get('decay_time', 2.0),

                        section_length, 'ease_out'

                    ),

                    'start_value': round(reverb_settings.get('decay_time', 2.0) * 0.85, 2),

                    'end_value': round(reverb_settings.get('decay_time', 2.0), 2),

                })



            elif send_key == 'echo' and delay_settings:

                # Delay send level

                return_auto['parameters'].append({

                    'device': 'Echo',

                    'parameter': 'Dry/Wet',

                    'envelope': self._generate_automation_envelope(

                        delay_settings.get('send_level', 0.15) * 0.85,

                        delay_settings.get('send_level', 0.15) * energy_factor,

                        section_length, curve_name

                    ),

                    'start_value': round(delay_settings.get('send_level', 0.15) * 0.85, 3),

                    'end_value': round(delay_settings.get('send_level', 0.15) * energy_factor, 3),

                })

                # Feedback

                return_auto['parameters'].append({

                    'device': 'Echo',

                    'parameter': 'Feedback',

                    'envelope': self._generate_automation_envelope(

                        delay_settings.get('feedback', 0.3) * 0.8,

                        delay_settings.get('feedback', 0.3),

                        section_length, 'ramp_up'

                    ),

                    'start_value': round(delay_settings.get('feedback', 0.3) * 0.8, 3),

                    'end_value': round(delay_settings.get('feedback', 0.3), 3),

                })



            elif send_key == 'heat' and saturation_settings:

                # Saturation drive

                return_auto['parameters'].append({

                    'device': 'Saturator',

                    'parameter': 'Drive',

                    'envelope': self._generate_automation_envelope(

                        saturation_settings.get('drive', 2.0) * 0.6,

                        saturation_settings.get('drive', 2.0) * energy_factor,

                        section_length, 'ramp_up'

                    ),

                    'start_value': round(saturation_settings.get('drive', 2.0) * 0.6, 2),

                    'end_value': round(saturation_settings.get('drive', 2.0) * energy_factor, 2),

                })



            elif send_key == 'glue' and compression_settings:

                # Compressor threshold

                return_auto['parameters'].append({

                    'device': 'Compressor',

                    'parameter': 'Threshold',

                    'envelope': self._generate_automation_envelope(

                        compression_settings.get('threshold', -12.0) + 3,

                        compression_settings.get('threshold', -12.0) - (energy_factor - 1) * 2,

                        section_length, 'ease_in'

                    ),

                    'start_value': round(compression_settings.get('threshold', -12.0) + 3, 1),

                    'end_value': round(compression_settings.get('threshold', -12.0) - (energy_factor - 1) * 2, 1),

                })



            if return_auto['parameters']:

                automation_data['return_automation'].append(return_auto)



        # Build master automation

        automation_data['master_automation'] = {

            'stereo_width': {

                'parameter': 'Stereo Width',

                'envelope': self._generate_automation_envelope(

                    stereo_width_settings.get('value', 1.0) * 0.9,

                    stereo_width_settings.get('value', 1.0),

                    section_length, 'ease_in_out'

                ),

                'start_value': round(stereo_width_settings.get('value', 1.0) * 0.9, 3),

                'end_value': round(stereo_width_settings.get('value', 1.0), 3),

            },

            'compression': {

                'parameter': 'Ratio',

                'envelope': self._generate_automation_envelope(

                    compression_settings.get('ratio', 2.0) * 0.8,

                    compression_settings.get('ratio', 2.0) * energy_factor,

                    section_length, 'ease_in'

                ),

                'start_value': round(compression_settings.get('ratio', 2.0) * 0.8, 2),

                'end_value': round(compression_settings.get('ratio', 2.0) * energy_factor, 2),

            },

        }



        return automation_data



    def _build_full_automation_blueprint(

        self,

        sections: List[Dict[str, Any]],

        buses: List[Dict[str, Any]],

        returns: List[Dict[str, Any]]

    ) -> List[Dict[str, Any]]:

        """

        Build complete automation blueprint for all sections.



        Args:

            sections: List of section configurations

            buses: List of bus track configurations

            returns: List of return track configurations



        Returns:

            List of automation data dictionaries, one per section

        """

        automation_blueprint = []



        for section in sections:

            section_automation = self._build_section_automation(section, buses, returns)

            automation_blueprint.append(section_automation)



        return automation_blueprint



    def _build_master_state(self, section_kind: str) -> Dict[str, Any]:

        """

        Build master chain state for a section.



        Returns a snapshot payload with flat device parameters for master chain.

        """

        section = section_kind.lower()

        device_parameters = []

        for device_name, parameter_map in MASTER_DEVICE_AUTOMATION.items():

            for parameter_name, section_values in parameter_map.items():

                value = section_values.get(section, section_values.get('drop', 0.0))

                clamp = MASTER_SAFETY_CLAMPS.get(parameter_name)

                if clamp:

                    value = max(clamp['min'], min(clamp['max'], float(value)))

                device_parameters.append({

                    'device_name': device_name,

                    'parameter': parameter_name,

                    'value': round(float(value), 3),

                })



        return {

            'section': section,

            'device_parameters': device_parameters,

        }



    def _build_device_parameters_for_role(self, role: str, section_kind: str) -> List[Dict[str, Any]]:

        """

        Build flat device parameter automation entries for a track role in a section.

        """

        role_lower = role.lower().replace(' ', '_').replace('-', '_')

        if role_lower not in SECTION_DEVICE_AUTOMATION:

            return []

        section = section_kind.lower()

        device_params = []

        for device_name, parameter_map in SECTION_DEVICE_AUTOMATION.get(role_lower, {}).items():

            for parameter_name, section_values in parameter_map.items():

                value = section_values.get(section, section_values.get('drop', 0.0))

                clamp = DEVICE_PARAMETER_SAFETY_CLAMPS.get(parameter_name)

                if clamp:

                    value = max(clamp['min'], min(clamp['max'], float(value)))

                device_params.append({

                    'device_name': device_name,

                    'parameter': parameter_name,

                    'value': round(float(value), 3),

                })

        return device_params



    def _build_bus_device_parameters(self, bus_key: str, section_kind: str) -> List[Dict[str, Any]]:

        """

        Build flat device parameter automation entries for a bus track in a section.

        Uses BUS_DEVICE_AUTOMATION constant for per-section values.

        """

        bus_key_lower = bus_key.lower()

        if bus_key_lower not in BUS_DEVICE_AUTOMATION:

            return []

        section = section_kind.lower()

        device_params = []

        for device_name, parameter_map in BUS_DEVICE_AUTOMATION.get(bus_key_lower, {}).items():

            for parameter_name, section_values in parameter_map.items():

                value = section_values.get(section, section_values.get('drop',0.0))

                clamp = DEVICE_PARAMETER_SAFETY_CLAMPS.get(parameter_name)

                if clamp:

                    value = max(clamp['min'], min(clamp['max'], float(value)))

                device_params.append({

                    'device_name': device_name,

                    'parameter': parameter_name,

                    'value': round(float(value), 3),

                })

        return device_params



    def _build_performance_snapshots(self, blueprint_tracks: List[Dict[str, Any]],

                                     sections: List[Dict[str, Any]],

                                     returns: Optional[List[Dict[str, Any]]] = None,

                                     buses: Optional[List[Dict[str, Any]]] = None,

                                     reference_energy_profile: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:

        performance = []

        stereo_roles = {'hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'pad', 'pluck', 'arp', 'counter', 'reverse_fx', 'riser', 'impact', 'atmos', 'vocal'}

        profile_pan_width = float(self._current_generation_profile.get('pan_width', 0.12))

        volume_factors = {

            'intro': 0.86,

            'build': 0.94,

            'drop': 1.02,

            'break': 0.78,

            'outro': 0.8,

        }



        # Build energy profile lookup by section index for adaptive mixing

        energy_by_index = {}

        if reference_energy_profile:

            for i, ep in enumerate(reference_energy_profile):

                energy_by_index[i] = ep.get('energy_mean', 0.5)

        else:

            # Fallback: use section features if available

            for i, section in enumerate(sections):

                features = section.get('features', {})

                energy_by_index[i] = features.get('energy_mean', features.get('energy', 0.5))



        space_send_factors = {

            'intro': 1.15,

            'build': 1.0,

            'drop': 0.82,

            'break': 1.35,

            'outro': 1.05,

        }

        echo_send_factors = {

            'intro': 1.08,

            'build': 1.18,

            'drop': 0.78,

            'break': 1.45,

            'outro': 0.95,

        }

        heat_send_factors = {

            'intro': 0.55,

            'build': 0.92,

            'drop': 1.18,

            'break': 0.42,

            'outro': 0.72,

        }

        glue_send_factors = {

            'intro': 0.72,

            'build': 0.96,

            'drop': 1.08,

            'break': 0.58,

            'outro': 0.78,

        }



        for section_idx, section in enumerate(sections):

            kind = str(section.get('kind', 'drop')).lower()

            energy = max(1, int(section.get('energy', 1)))



            # Get energy_mean from reference profile for adaptive volume scaling

            ref_energy_mean = energy_by_index.get(section_idx, 0.5)



            snapshot = {

                'scene_index': int(section.get('index', len(performance))),

                'name': section.get('name', "SECTION"),

                'track_states': [],

                'return_states': self._build_return_states(list(returns or []), section),

                'bus_states': [],

            }



            for track_index, track_data in enumerate(blueprint_tracks):

                role = track_data.get('role', '')

                base_volume = float(track_data.get('volume', 0.72))

                base_pan = float(track_data.get('pan', 0.0))

                base_sends = dict(track_data.get('sends', {}))

                intensity = self._role_intensity(role, section)

                is_muted = role != 'sc_trigger' and intensity <= 0



                if is_muted:

                    target_volume = round(base_volume * 0.08, 3)

                else:

                    factor = volume_factors.get(kind, 1.0) + max(0.0, (energy - 3) * 0.03)

                    if role in ['kick', 'sub_bass', 'bass'] and kind == 'drop':

                        factor += 0.04

                    if role in ['pad', 'atmos', 'drone'] and kind == 'break':

                        factor += 0.08

                    if role in ['reverse_fx', 'riser', 'impact'] and kind in ['build', 'break']:

                        factor += 0.06 * float(self._current_generation_profile.get('fx_bias', 1.0))



                    # Apply energy-based volume scaling from reference profile

                    if ref_energy_mean < 0.3:

                        # Quiet sections (intro, quiet breaks) - reduce volume

                        energy_volume_factor = 0.85

                    elif ref_energy_mean > 0.7:

                        # High energy sections (drops, peaks) - boost volume

                        energy_volume_factor = 1.08

                    else:

                        energy_volume_factor = 1.0



                    target_volume = round(min(1.0, max(0.0, base_volume * factor * energy_volume_factor)), 3)



                target_pan = base_pan

                pan_variant = str(section.get('pan_variant', 'narrow')).lower()

                if role in stereo_roles:

                    if pan_variant == 'tilt_left':

                        direction = -1.0

                        width = profile_pan_width

                    elif pan_variant == 'tilt_right':

                        direction = 1.0

                        width = profile_pan_width

                    elif pan_variant == 'wide':

                        direction = -1.0 if track_index % 2 == 0 else 1.0

                        width = profile_pan_width * 1.1

                    else:

                        direction = -1.0 if track_index % 2 == 0 else 1.0

                        width = profile_pan_width * 0.55



                    if kind == 'break':

                        width *= 1.18

                    elif kind == 'drop':

                        width *= 0.92

                    target_pan = self._clamp_pan(base_pan + (direction * width))



                target_sends = {}

                for send_name, send_value in base_sends.items():

                    send_factor = 1.0

                    if send_name == 'space':

                        send_factor = space_send_factors.get(kind, 1.0)

                    elif send_name == 'echo':

                        send_factor = echo_send_factors.get(kind, 1.0)

                    elif send_name == 'heat':

                        send_factor = heat_send_factors.get(kind, 1.0)

                    elif send_name == 'glue':

                        send_factor = glue_send_factors.get(kind, 1.0)



                    if role in ['riser', 'impact'] and kind in ['build', 'break']:

                        send_factor += 0.18

                    if role == 'vocal' and kind in ['build', 'drop']:

                        send_factor += 0.12

                    if role in ['kick', 'sub_bass', 'bass'] and send_name in ['heat', 'glue'] and kind == 'drop':

                        send_factor += 0.1

                    if is_muted:

                        send_factor *= 0.25



                    target_sends[send_name] = round(min(1.0, max(0.0, float(send_value) * send_factor)), 3)



                track_state = {

                    'track_index': track_index,

                    'role': role,

                    'mute': is_muted,

                    'volume': target_volume,

                    'pan': target_pan,

                    'sends': target_sends,

                }



                # Add device_parameters to track state

                device_params = self._build_device_parameters_for_role(role, kind)

                if device_params:

                    track_state['device_parameters'] = device_params



                snapshot['track_states'].append(track_state)



            # Add bus states to snapshot

            for bus_data in list(buses or []):

                bus_key = str(bus_data.get('key', '')).lower()

                if not bus_key:

                    continue

                bus_device_params = self._build_bus_device_parameters(bus_key, kind)

                if bus_device_params:

                    bus_state = {

                        'bus_key': bus_key,

                        'bus_name': bus_data.get('name', bus_key.upper()),

                        'device_parameters': bus_device_params,

                    }

                    snapshot['bus_states'].append(bus_state)



            # Add master state to snapshot

            master_state = self._build_master_state(kind)

            if master_state.get('device_parameters'):

                snapshot['master_state'] = master_state



            performance.append(snapshot)



        return performance



    def _build_mix_automation_summary(self, performance: List[Dict]) -> Dict[str, Any]:

        """

        Build summary of automation in performance snapshots.



        Returns:

            - track_snapshots_with_device_automation: count

            - return_snapshots_with_device_automation: count

            - bus_snapshots_with_device_automation: count

            - master_snapshots_count: count

            - track_roles_touched: list of roles with device automation

            - bus_keys_touched: list of bus keys with device automation

            - master_parameters_touched: list of master params automated

        """

        track_count = 0

        return_count = 0

        bus_count = 0

        master_count = 0

        track_roles = set()

        bus_keys = set()

        master_params = set()



        for snapshot in performance:

            # Check track states

            for track_state in snapshot.get('track_states', []):

                if 'device_parameters' in track_state and track_state['device_parameters']:

                    track_count += 1

                    role = track_state.get('role', 'unknown')

                    track_roles.add(role)



            # Check return states

            for return_state in snapshot.get('return_states', []):

                if 'device_parameters' in return_state and return_state['device_parameters']:

                    return_count += 1



            # Check bus states

            for bus_state in snapshot.get('bus_states', []):

                if 'device_parameters' in bus_state and bus_state['device_parameters']:

                    bus_count += 1

                    bus_key = bus_state.get('bus_key', 'unknown')

                    bus_keys.add(bus_key)



            # Check master state

            master_state = snapshot.get('master_state', {})

            if master_state.get('device_parameters'):

                master_count += 1

                for item in master_state.get('device_parameters', []):

                    param_name = str(item.get('parameter', '') or '').strip()

                    if param_name:

                        master_params.add(param_name)



        return {

            'track_snapshots_with_device_automation': track_count,

            'return_snapshots_with_device_automation': return_count,

            'bus_snapshots_with_device_automation': bus_count,

            'master_snapshots_count': master_count,

            'track_roles_touched': sorted(list(track_roles)),

            'bus_keys_touched': sorted(list(bus_keys)),

            'master_parameters_touched': sorted(list(master_params))

        }



    def _verify_automation_safety(self, performance: List[Dict]) -> List[str]:

        """

        Verify automation values are within safe ranges.



        Returns list of warnings if any values are outside safe ranges.

        """

        warnings = []



        for i, snapshot in enumerate(performance):

            # Check master state

            master_state = snapshot.get('master_state', {})

            for item in master_state.get('device_parameters', []):

                device_name = str(item.get('device_name', 'unknown'))

                param_name = str(item.get('parameter', '') or '').strip()

                value = float(item.get('value', 0.0))

                clamp = MASTER_SAFETY_CLAMPS.get(param_name)

                if clamp and (value < clamp['min'] or value > clamp['max']):

                    warnings.append(f"Snapshot {i}: {device_name}.{param_name}={value} outside safe range [{clamp['min']}, {clamp['max']}]")



        return warnings



    def _build_gain_staging_summary(self, config: Dict[str, Any]) -> Dict[str, Any]:

        """

        Build gain staging summary for the generated config.

        """

        warnings = []



        # Check bus volumes for extreme values

        bus_volumes = self._calibrated_bus_volumes or {}

        for bus_name, vol in bus_volumes.items():

            if vol > 0.9:

                warnings.append(f"Bus {bus_name} volume > 0.9: {vol:.3f}")



        # Check master limiter gain

        master = config.get('master', {})

        master_limiter_gain = 0.0

        for device in master.get('device_chain', []):

            if device.get('device') == 'Limiter':

                master_limiter_gain = device.get('parameters', {}).get('Gain', 0.0)

        if master_limiter_gain > 1.0:

            warnings.append(f"Master limiter gain > 1.0: {master_limiter_gain:.3f}")



        # Check track volumes

        for track in config.get('tracks', []):

            vol = track.get('volume', 0.0)

            role = track.get('role', 'unknown')

            if vol > 0.9:

                warnings.append(f"Track {role} volume > 0.9: {vol:.3f}")



        return {

            'master_profile_used': getattr(self, '_master_profile_used', 'default'),

            'style_adjustments_applied': getattr(self, '_style_adjustments_applied', []),

            'bus_volumes': bus_volumes,

            'track_volume_overrides_count': getattr(self, '_gain_calibration_overrides_count', 0),

            'peak_reductions_applied_count': getattr(self, '_peak_reductions_count', 0),

            'headroom_target_db': TARGET_HEADROOM_DB,

            'warnings': warnings,

        }



    def generate_config(self, genre: str, style: str = "", bpm: float = 0,

                        key: str = "", structure: str = "standard",

                        palette: Optional[Dict[str, str]] = None,

                        reference_context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:

        """

        Genera una configuraciÃ³n completa de track



        Args:

            genre: GÃ©nero musical

            style: Sub-estilo

            bpm: BPM (0 = auto)

            key: Tonalidad ("" = auto)

            structure: Tipo de estructura

            palette: Palette de carpetas para selecciÃ³n de samples

            reference_context: Contexto de referencia audio analizado (opcional)
                Incluye: phrase_plan, musical_theme, harmonic_instrument_hints, 
                micro_stem_summary, synth_loop_hint

        """

        genre = genre.lower().replace(' ', '-')

        style = style.lower() if style else ""

        variant_seed = random.SystemRandom().randint(1000, 999999)

        random.seed(variant_seed)



        # Decay pattern variant memory to allow reuse

        _decay_pattern_variant_memory()



        # Reset gain staging counters

        self._gain_calibration_overrides_count = 0

        self._peak_reductions_count = 0

        self._style_adjustments_applied = []

        self._calibrated_bus_volumes = {}

        self._master_profile_used = 'default'



        reference_resolution = self._resolve_reference_track_profile(genre, style, bpm, key, structure)

        if reference_resolution:

            genre = reference_resolution.get('genre', genre) or genre

            style = reference_resolution.get('style', style)

            bpm = float(reference_resolution.get('bpm', bpm or 0))

            key = reference_resolution.get('key', key)

            structure = reference_resolution.get('structure', structure)



        # Obtener configuraciÃ³n del gÃ©nero

        genre_config = GENRE_CONFIGS.get(genre, GENRE_CONFIGS['techno'])



        # Determinar BPM

        if bpm <= 0:

            bpm = genre_config['default_bpm']



        # Determinar key

        if not key:

            key = random.choice(genre_config['keys'])



        # Determinar estilo si no se especificÃ³

        if not style:

            style = random.choice(genre_config['styles'])



        # Parsear key

        _root_note = key[:-1] if len(key) > 1 else key  # noqa: F841 - parsed when needed per section

        is_minor = 'm' in key.lower()

        scale = 'minor' if is_minor else 'major'

        profile = self._build_arrangement_profile(genre, style, variant_seed)

        profile['style_text'] = f"{genre} {style}".strip().lower()

        profile['reference_name'] = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()

        self._current_generation_profile = profile

        sections = self._build_sections(structure, style, variant_seed, profile)



        # Initialize musical theme for coherent phrase generation

        # Check for external reference context (from reference audio analysis)

        external_phrase_plan = None

        external_musical_theme = None

        external_harmonic_hints = None

        external_primary_family = None

        if reference_context:

            external_phrase_plan = reference_context.get('phrase_plan')

            external_musical_theme = reference_context.get('musical_theme')

            external_harmonic_hints = reference_context.get('harmonic_instrument_hints')

            external_primary_family = PhrasePlan._normalize_family_name(
                reference_context.get('primary_harmonic_family')
            )
            if not external_primary_family and external_harmonic_hints:
                for token in ('pluck', 'piano', 'keys', 'pad', 'lead'):
                    if external_harmonic_hints.get(token):
                        external_primary_family = token
                        break

            if external_phrase_plan:

                self.logger.info(f"[HYBRID] Using external phrase plan from reference: {len(external_phrase_plan.get('phrases', []))} phrases")

            if external_musical_theme:

                self.logger.info(f"[HYBRID] Using external musical theme from reference: key={external_musical_theme.get('key', 'unknown')}")

            if external_harmonic_hints:

                available = list(external_harmonic_hints.keys())

                self.logger.info(f"[HYBRID] Using external harmonic hints from reference: {available}")

        

        # Initialize musical theme (use external if available)

        if external_musical_theme:

            # Restore external musical theme

            self.musical_theme = MusicalTheme(

                key=external_musical_theme.get('key', key),

                scale=external_musical_theme.get('scale', scale),

                seed=external_musical_theme.get('seed', variant_seed)

            )
            external_base_motif = list(external_musical_theme.get('base_motif', []) or [])
            if external_base_motif:
                self.musical_theme.base_motif = external_base_motif

        else:

            self.initialize_musical_theme(key=key, scale=scale, seed=variant_seed)

        

        # Create or restore phrase plan for hybrid materialization

        phrase_plan = None

        if external_phrase_plan:

            # Restore external phrase plan from dict

            try:
                phrase_plan = PhrasePlan.from_dict(external_phrase_plan, sections_override=sections)
                if not phrase_plan.primary_harmonic_family and external_primary_family:
                    phrase_plan.primary_harmonic_family = external_primary_family

                self.logger.info(f"[HYBRID] External phrase plan restored: {len(phrase_plan.phrases)} phrases")

            except Exception as e:

                self.logger.warning(f"[HYBRID] Failed to restore external phrase plan: {e}")

                phrase_plan = None

        

        # Create local phrase plan if no external one available

        if not phrase_plan and self.musical_theme:

            base_motif = self.musical_theme.base_motif

            phrase_plan = PhrasePlan(

                base_motif=base_motif,

                sections=sections,

                key=key,

                scale=scale,

                seed=variant_seed,

                primary_harmonic_family=external_primary_family

            )

            self.logger.info(f"[HYBRID] Local phrase plan created: {len(phrase_plan.phrases)} phrases across {len(sections)} sections")



        # Crear configuraciÃ³n base

        config = {

            'name': f"{genre.title()} {style.title()}",

            'bpm': bpm,

            'key': key,

            'scale': scale,

            'genre': genre,

            'style': style,

            'structure': structure,

            'variant_seed': variant_seed,

            'arrangement_profile': profile['name'],

            'reference_track': reference_resolution.get('reference') if reference_resolution else None,

            'reference_energy_profile': reference_resolution.get('reference_energy_profile') if reference_resolution else None,

            'auto_generate': True,

            'sections': sections,

            'buses': self._build_mix_bus_blueprint(profile, genre, style, reference_resolution),

            'returns': self._build_return_blueprint(profile, genre, style, reference_resolution),

            'master': self._build_master_blueprint(profile, genre, style, reference_resolution),

            'phrase_plan': phrase_plan.to_dict() if phrase_plan else None,

            'primary_harmonic_family': (
                phrase_plan.primary_harmonic_family if phrase_plan else external_primary_family
            ),

            'hybrid_materialization': {

                'enabled': True,

                'types_supported': ['midi_phrase', 'preset_hint', 'audio_loop'],

                'phrase_count': len(phrase_plan.phrases) if phrase_plan else 0,

            },

            'palette': palette or {},

            'tracks': [],

        }



        # Generar tracks segÃºn gÃ©nero

        config['tracks'] = self._generate_tracks_for_genre(genre, style, key, scale, structure, sections, profile, phrase_plan, external_harmonic_hints)

        config['performance'] = self._build_performance_snapshots(config['tracks'], sections, config.get('returns', []), config.get('buses', []))

        config['mix_automation_summary'] = self._build_mix_automation_summary(config['performance'])

        config['mix_automation_warnings'] = self._verify_automation_safety(config['performance'])

        config['gain_staging_summary'] = self._build_gain_staging_summary(config)

        config['automation'] = self._build_full_automation_blueprint(sections, config.get('buses', []), config.get('returns', []))

        config['transition_events'] = self._generate_transition_events(sections)



        # Apply density rules to prevent overcrowding

        config['transition_events'] = self._apply_transition_density_rules(config['transition_events'], sections)



        # Materialize transition events into track blueprints

        config['tracks'] = self._materialize_transition_events(config, config['tracks'])



        config['locators'] = self._build_locators(sections)

        config['total_bars'] = sum(section['bars'] for section in sections)

        config['total_beats'] = float(config['total_bars'] * 4)



        # Add section variants summary

        config['section_variants'] = {

            section.get('name', f'section_{i}'): {

                'kind': section.get('kind', 'unknown'),

                'drum_variant': section.get('drum_variant', 'straight'),

                'kick_variant': section.get('kick_variant', (section.get('drum_role_variants') or {}).get('kick', 'straight')),

                'clap_variant': section.get('clap_variant', (section.get('drum_role_variants') or {}).get('clap', 'straight')),

                'hat_closed_variant': section.get('hat_closed_variant', (section.get('drum_role_variants') or {}).get('hat_closed', 'straight')),

                'bass_variant': section.get('bass_variant', 'anchor'),

                'bass_bank_variant': section.get('bass_bank_variant', section.get('bass_variant', 'anchor')),

                'melodic_variant': section.get('melodic_variant', 'motif'),

                'melodic_bank_variant': section.get('melodic_bank_variant', section.get('melodic_variant', 'motif')),

                'transition_fill': section.get('transition_fill', 'none'),

            }

            for i, section in enumerate(sections)

        }
        
        # Add budget tracking to manifest
        config['budget_tracking'] = self._get_budget_tracking()
        
        # Add generation context for hybrid materialization tracking
        config['generation_context'] = {
            'had_phrase_plan': phrase_plan is not None,
            'had_harmonic_hints': external_harmonic_hints is not None if reference_context else False,
            'had_musical_theme': external_musical_theme is not None if reference_context else False,
            'phrase_count': len(phrase_plan.phrases) if phrase_plan else 0,
            'external_source': reference_context is not None,
        }
        
        # Log final generation context status
        self.logger.info(f"[HYBRID] Generation context: phrase_plan={config['generation_context']['had_phrase_plan']}, "
                          f"harmonic_hints={config['generation_context']['had_harmonic_hints']}, "
                          f"phrase_count={config['generation_context']['phrase_count']}")
        
        return config



    def _build_locators(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:

        locators = []

        arrangement_time = 0.0

        for section in sections:

            locators.append({

                'scene_index': int(section.get('index', len(locators))),

                'name': section.get('name', 'SECTION'),

                'bars': int(section.get('bars', 8)),

                'color': int(section.get('color', 10)),

                'time_beats': round(arrangement_time, 3),

            })

            arrangement_time += float(section.get('beats', 0.0) or 0.0)

        return locators



    def _generate_tracks_for_genre(self, genre: str, style: str, key: str,
                                   scale: str, structure: str, sections: List[Dict[str, Any]],
                                   profile: Optional[Dict[str, Any]] = None,
                                   phrase_plan: Optional['PhrasePlan'] = None,
                                   harmonic_hints: Optional[Dict[str, Any]] = None) -> List[Dict]:
        """Genera la configuración de tracks según el género con soporte para materialización híbrida."""
        # Reset track budget at start of generation
        self._reset_track_budget()
        
        track_specs = []
        style_text = f"{genre} {style}".lower()



        if genre == 'reggaeton':

            track_specs.extend([

                ('SC TRIGGER', 'sc_trigger', TRACK_COLORS['technical'], 'operator'),

                ('KICK', 'kick', TRACK_COLORS['kick'], 'operator'),

                ('CLAP', 'clap', TRACK_COLORS['clap'], 'operator'),

                ('SNARE FILL', 'snare_fill', TRACK_COLORS['snare'], 'operator'),

                ('HAT CLOSED', 'hat_closed', TRACK_COLORS['hat'], 'operator'),

                ('HAT OPEN', 'hat_open', TRACK_COLORS['hat'], 'operator'),

                ('TOP LOOP', 'top_loop', TRACK_COLORS['hat'], 'operator'),

                ('PERCUSSION', 'perc', TRACK_COLORS['perc'], 'operator'),

                ('SUB BASS', 'sub_bass', TRACK_COLORS['bass'], 'operator'),

                ('BASS', 'bass', TRACK_COLORS['bass'], 'operator'),

                ('CHORDS', 'chords', TRACK_COLORS['chords'], 'wavetable'),

                ('PLUCK', 'pluck', TRACK_COLORS['synth'], 'wavetable'),

                # VOCAL CHOP removed - vocal is manual-only

                ('PAD', 'pad', TRACK_COLORS['pad'], 'wavetable'),

                ('IMPACT FX', 'impact', TRACK_COLORS['fx'], 'operator'),

                ('ATMOS', 'atmos', TRACK_COLORS['fx'], 'analog'),

            ])

        else:

            track_specs.extend([

                ('SC TRIGGER', 'sc_trigger', TRACK_COLORS['technical'], 'operator'),

                ('KICK', 'kick', TRACK_COLORS['kick'], 'operator'),

                ('CLAP', 'clap', TRACK_COLORS['clap'], 'operator'),

                ('SNARE FILL', 'snare_fill', TRACK_COLORS['snare'], 'operator'),

                ('HAT CLOSED', 'hat_closed', TRACK_COLORS['hat'], 'operator'),

                ('HAT OPEN', 'hat_open', TRACK_COLORS['hat'], 'operator'),

                ('TOP LOOP', 'top_loop', TRACK_COLORS['hat'], 'operator'),

                ('PERCUSSION', 'perc', TRACK_COLORS['perc'], 'operator'),

                ('TOM FILL', 'tom_fill', TRACK_COLORS['perc'], 'operator'),

                ('SUB BASS', 'sub_bass', TRACK_COLORS['bass'], 'operator'),

                ('BASS', 'bass', TRACK_COLORS['bass'], 'operator'),

                ('DRONE', 'drone', TRACK_COLORS['pad'], 'analog'),

                ('CHORDS', 'chords', TRACK_COLORS['chords'], 'wavetable'),

                ('STAB', 'stab', TRACK_COLORS['synth'], 'operator'),

                ('PAD', 'pad', TRACK_COLORS['pad'], 'wavetable'),

                ('ARP', 'arp', TRACK_COLORS['synth'], 'operator'),

                ('LEAD', 'lead', TRACK_COLORS['synth'], 'wavetable'),

                ('COUNTER', 'counter', TRACK_COLORS['synth'], 'operator'),

                ('CRASH', 'crash', TRACK_COLORS['fx'], 'operator'),

                ('REVERSE FX', 'reverse_fx', TRACK_COLORS['fx'], 'analog'),

                ('RISER FX', 'riser', TRACK_COLORS['fx'], 'operator'),

                ('IMPACT FX', 'impact', TRACK_COLORS['fx'], 'operator'),

                ('ATMOS', 'atmos', TRACK_COLORS['fx'], 'analog'),

            ])

        tracks = []



        # Synths/Chords segÃºn gÃ©nero

        if genre in ['house', 'trance', 'progressive']:

            tracks.append(self._generate_chord_track(key, scale, genre))

            tracks.append(self._generate_lead_track(key, scale, genre))

        elif genre in ['techno', 'tech-house']:

            if random.random() > 0.3:  # 70% de probabilidad

                tracks.append(self._generate_chord_track(key, scale, genre))

            if random.random() > 0.5:

                tracks.append(self._generate_lead_track(key, scale, genre))



        # FX/AtmÃ³sfera para estructuras extended

        if structure in ['extended', 'club'] or random.random() > 0.6:

            tracks.append(self._generate_fx_track())



        if genre in ['techno', 'tech-house', 'trance']:

            track_specs.insert(8, ('RIDE', 'ride', TRACK_COLORS['ride'], 'operator'))

        if genre in ['house', 'tech-house', 'trance'] or 'latin' in style_text:

            track_specs.insert(14, ('PLUCK', 'pluck', TRACK_COLORS['synth'], 'wavetable'))
            # VOCAL CHOP removed - vocal is manual-only

        elif genre == 'drum-and-bass':

            track_specs = [

                ('BREAK', 'kick', TRACK_COLORS['kick'], 'operator'),

                ('SNARE', 'clap', TRACK_COLORS['snare'], 'operator'),

                ('HATS', 'hat_closed', TRACK_COLORS['hat'], 'operator'),

                ('PERCUSSION', 'perc', TRACK_COLORS['perc'], 'operator'),

                ('SUB BASS', 'sub_bass', TRACK_COLORS['bass'], 'operator'),

                ('REESE', 'bass', TRACK_COLORS['bass'], 'operator'),

                ('PAD', 'pad', TRACK_COLORS['pad'], 'wavetable'),

                ('ARP', 'arp', TRACK_COLORS['synth'], 'operator'),

('LEAD', 'lead', TRACK_COLORS['synth'], 'wavetable'),

                # VOCAL removed - vocal is manual-only

                ('RISER FX', 'riser', TRACK_COLORS['fx'], 'operator'),

                ('ATMOS', 'atmos', TRACK_COLORS['fx'], 'analog'),

            ]



        blueprint_tracks = []
        active_profile = dict(profile or self._current_generation_profile or {'name': 'default'})
        
        for name, role, color, device in track_specs:
            # BUDGET ENFORCEMENT: Check if we can create this track
            if not self._can_create_track(role):
                continue
            
            clips = self._build_scene_clips(role, genre, style, key, scale, sections, phrase_plan, harmonic_hints)
            if not clips:
                continue


            mix_profile = dict(ROLE_MIX.get(role, {}))
            mix_profile['sends'] = self._extend_parallel_sends(role, mix_profile.get('sends', {}))
            mix_profile = self._shape_mix_profile(role, mix_profile, active_profile, style)
            track = {

                'name': name,

                'type': 'midi',

                'role': role,

                'bus': self._resolve_bus_for_role(role),

                'device': device,

                'color': color,

                'volume': mix_profile.get('volume', 0.72),

                'pan': mix_profile.get('pan', 0.0),

                'sends': dict(mix_profile.get('sends', {})),

                'fx_chain': self._shape_role_fx_chain(role, active_profile, style),

                'clips': clips,

            }

            track['clip'] = dict(clips[0])



            # Agregar metadata de variación al blueprint

            if role in SECTION_VARIATION_CONFIG:

                track['section_variation'] = SECTION_VARIATION_CONFIG[role]

                track['can_vary_by_section'] = True

            
            # BUDGET TRACKING: Record track creation
            self._track_created(role, name, 'midi')


            blueprint_tracks.append(track)
        
        # BUDGET SUMMARY: Log final budget usage
        self._log_budget_summary()


        return blueprint_tracks

    def _build_sections(self, structure: str, style: str = "", variant_seed: Optional[int] = None,
                        profile: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
        structure_key = structure.lower()
        rng = random.Random(variant_seed) if variant_seed is not None else random
        
        # P1 Sprint v0.1.23: Auto-select reggaeton-specific structure for perreo/safaera styles
        style_lower = (style or "").lower()
        is_reggaeton_style = any(term in style_lower for term in ('perreo', 'safaera', 'dembow', 'latin', 'reggaeton'))
        
        # Determine which blueprint set to use
        if structure_key == 'standard' and is_reggaeton_style:
            # Auto-select from reggaeton blueprints for perreo styles
            reggaeton_variants = ['reggaeton', 'perreo_duro', 'safaera_style']
            structure_key = rng.choice(reggaeton_variants)
        elif structure_key in ('reggaeton', 'perreo_duro', 'safaera_style'):
            # Already explicitly set, use as-is
            pass
        
        blueprint_options = SECTION_BLUEPRINT_VARIANTS.get(structure_key)

        if blueprint_options:

            if 'latin' in style and structure_key == 'club' and len(blueprint_options) > 1:

                blueprint = rng.choice(blueprint_options[1:])

            else:

                blueprint = rng.choice(blueprint_options)

        else:

            blueprint = SECTION_BLUEPRINTS.get(structure_key, SECTION_BLUEPRINTS['standard'])

        sections = []

        style_text = style.lower() if style else ""

        profile_name = str((profile or {}).get('name', 'default')).lower()

        for index, (name, bars, color, kind, energy) in enumerate(blueprint):

            if kind == 'intro':

                drum_variants = ['straight', 'skip']

                bass_variants = ['anchor', 'pedal']

                melodic_variants = ['motif', 'response']

            elif kind == 'build':

                drum_variants = ['shuffle', 'pressure', 'straight']

                bass_variants = ['bounce', 'syncopated']

                melodic_variants = ['lift', 'response']

            elif kind == 'break':

                drum_variants = ['skip', 'shuffle']

                bass_variants = ['pedal', 'anchor']

                melodic_variants = ['drone', 'response']

            elif kind == 'outro':

                drum_variants = ['straight', 'skip']

                bass_variants = ['anchor', 'pedal']

                melodic_variants = ['motif', 'descend']

            else:

                drum_variants = ['straight', 'pressure', 'shuffle']

                bass_variants = ['syncopated', 'bounce', 'anchor']

                melodic_variants = ['lift', 'motif', 'descend']



            swing_pool = [0.0, 0.015, 0.025]

            if 'latin' in style_text or profile_name in ['jackin', 'swing']:

                swing_pool.extend([0.035, 0.045, 0.055])



            pan_variant = rng.choice(['narrow', 'wide', 'tilt_left', 'tilt_right'])

            if kind in ['intro', 'outro'] and rng.random() > 0.5:

                pan_variant = 'narrow'

            if kind == 'break' and rng.random() > 0.4:

                pan_variant = 'wide'



            section_data = {

                'index': index,

                'name': name,

                'bars': int(bars),

                'beats': float(bars * 4),

                'color': color,

                'kind': kind,

                'energy': int(energy),

                'density': round(min(1.35, max(0.68, 0.78 + (energy * 0.08) + rng.uniform(-0.08, 0.14))), 3),

                'swing': round(rng.choice(swing_pool), 3),

                'tension': int(min(5, max(1, energy + rng.choice([-1, 0, 0, 1])))),

                'drum_variant': rng.choice(drum_variants),

                'bass_variant': rng.choice(bass_variants),

                'melodic_variant': rng.choice(melodic_variants),

                'pan_variant': pan_variant,

                'transition_fill': rng.choice(['none', 'snare', 'tom', 'reverse', 'impact']),

            }

            sections.append(self._ensure_section_pattern_variants(section_data))

        # Check for excessive repetition and force variation if needed

        sections = self._check_section_repetition(sections)

        return sections



    def _role_intensity(self, role: str, section: Dict[str, Any]) -> int:

        kind = section.get('kind', 'drop')

        energy = int(section.get('energy', 1))

        role_energy = ROLE_ACTIVITY.get(role, {}).get(kind, 0)

        return min(max(role_energy, 0), max(1, energy + 1))



    def _build_scene_clips(self, role: str, genre: str, style: str, key: str,
                           scale: str, sections: List[Dict[str, Any]],
                           phrase_plan: Optional['PhrasePlan'] = None,
                           harmonic_hints: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
        """Build scene clips with optional phrase plan for hybrid materialization."""
        
        # Log when harmonic hints are available for this role
        if harmonic_hints and role in ['chords', 'stab', 'pad', 'pluck', 'arp', 'lead', 'counter']:
            available = list(harmonic_hints.keys())
            self.logger.info(f"[HARMONIC_HINTS_WIRING] _build_scene_clips received hints for {role}: {available}")
        clips = []
        for section in sections:
            notes = self._render_scene_notes(role, genre, style, key, scale, section, phrase_plan, harmonic_hints)
            if not notes:
                continue

            # Check for materialization hints in notes
            materialization_hint = None
            if notes and any('_preset_hint' in n for n in notes):
                materialization_hint = {
                    'type': 'preset_hint',
                    'preset_path': next((n['_preset_hint'] for n in notes if '_preset_hint' in n), None),
                    'family': next((n['_preset_family'] for n in notes if '_preset_family' in n), None),
                }
            elif phrase_plan and role in ['chords', 'stab', 'pad', 'pluck', 'arp', 'lead', 'counter']:
                # Check if phrase plan has MIDI phrase for this section
                phrase = next((p for p in phrase_plan.phrases 
                              if p.start <= section.get('start_bar', 0) < p.end 
                              and p.role == role), None)
                if phrase and phrase.notes:
                    materialization_hint = {
                        'type': 'midi_phrase',
                        'family': phrase.family,
                        'mutation': phrase.mutation_type,
                    }

            clip_data = {
                'scene_index': section['index'],
                'length': section['beats'],
                'name': f"{role.upper()} - {section['name']}",
                'notes': notes,
            }
            
            if materialization_hint:
                clip_data['materialization_hint'] = materialization_hint
                
            clips.append(clip_data)

        return clips



    def _render_scene_notes(self, role: str, genre: str, style: str, key: str,
                            scale: str, section: Dict[str, Any],
                            phrase_plan: Optional['PhrasePlan'] = None,
                            harmonic_hints: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
        """Render scene notes with optional phrase plan for hybrid materialization."""
        
        # Log harmonic hints propagation
        if harmonic_hints and role in ['chords', 'stab', 'pad', 'pluck', 'arp', 'lead', 'counter']:
            self.logger.info(f"[HARMONIC_HINTS_WIRING] _render_scene_notes received hints for {role} in {section.get('kind', 'unknown')}")
        intensity = self._role_intensity(role, section)
        if intensity <= 0:
            return []

        if role in ['sc_trigger', 'kick', 'clap', 'snare_fill', 'hat_closed', 'hat_open', 'top_loop', 'perc', 'tom_fill', 'ride', 'crash']:
            return self._render_drum_scene(role, genre, style, section, intensity)

        if role in ['sub_bass', 'bass']:
            return self._render_bass_scene(role, genre, style, key, section)

        if role in ['chords', 'stab', 'pad', 'pluck', 'arp', 'lead', 'counter']:
            # Pass phrase plan to enable hybrid materialization
            return self._render_musical_scene(role, genre, key, scale, section, phrase_plan, harmonic_hints)

        if role in ['drone', 'reverse_fx', 'riser', 'impact', 'atmos', 'vocal']:
            return self._render_fx_scene(role, key, section)

        return []



    def _render_drum_scene(self, role: str, genre: str, style: str,

                           section: Dict[str, Any], intensity: int) -> List[Dict[str, Any]]:

        total_length = float(section['beats'])

        kind = section['kind']

        style_text = f"{genre} {style}".lower()

        # T115: Check if we should use dembow groove templates
        use_dembow_groove = (genre == 'reggaeton' or
                            'dembow' in style_text or
                            'latin' in style_text or
                            'perreo' in style_text)

        # Get groove template if applicable
        groove_template = None
        if use_dembow_groove:
            try:
                from groove_extractor import get_dembow_groove
                groove_template = get_dembow_groove(bpm=None, section=kind)
            except ImportError:
                pass
        groove_micro_offset = 0.0
        if groove_template and groove_template.get('timing_variance_ms'):
            try:
                beat_duration = float(groove_template.get('beat_duration') or 0.0)
                timing_variance_seconds = max(
                    0.0,
                    float(groove_template.get('timing_variance_ms') or 0.0) / 1000.0,
                )
                if beat_duration > 0.0:
                    groove_micro_offset = min(0.06, (timing_variance_seconds / beat_duration) * 0.35)
            except (TypeError, ValueError):
                groove_micro_offset = 0.0


        if role == 'sc_trigger':

            pattern = [self._make_note(24, beat, 0.12, 127) for beat in [0.0, 1.0, 2.0, 3.0]]

            if kind == 'break':

                pattern = [self._make_note(24, beat, 0.1, 118) for beat in [0.0, 2.0]]

            return self._repeat_pattern(pattern, total_length, 4.0)



        if role == 'kick':

            if genre == 'drum-and-bass':

                pattern = [

                    self._make_note(36, 0.0, 0.25, 122),

                    self._make_note(36, 0.75, 0.2, 104),

                    self._make_note(36, 1.5, 0.2, 112),

                    self._make_note(36, 2.0, 0.25, 124),

                    self._make_note(36, 2.75, 0.2, 100),

                    self._make_note(36, 3.25, 0.2, 92),

                ]

            elif genre == 'reggaeton':

                if kind == 'break':

                    pattern = [

                        self._make_note(36, 0.0, 0.25, 120),

                        self._make_note(36, 2.0, 0.25, 112),

                    ]

                else:

                    # T115: Apply dembow groove template to kick positions
                    if groove_template and groove_template.get('kick_positions'):
                        kick_positions = groove_template['kick_positions']
                        kick_velocities = groove_template.get('kick_velocities', [0.9] * len(kick_positions))
                        pattern = []
                        for i, pos in enumerate(kick_positions):
                            if pos < 4.0:  # Within one bar
                                vel = int(100 + (kick_velocities[i] * 27)) if i < len(kick_velocities) else 110
                                pattern.append(self._make_note(36, pos, 0.25, min(127, vel)))
                    else:
                        # Fallback to default dembow pattern
                        pattern = [

                            self._make_note(36, 0.0, 0.25, 126),

                            self._make_note(36, 1.5, 0.2, 108),

                            self._make_note(36, 2.0, 0.25, 120),

                            self._make_note(36, 3.0, 0.2, 114),

                        ]

                    if intensity >= 3:

                        # Add ghost kick if not already in pattern
                        if not any(abs(n['start'] - 3.5) < 0.1 for n in pattern):
                            pattern.append(self._make_note(36, 3.5, 0.12, 88))

            elif kind == 'break':

                pattern = [

                    self._make_note(36, 0.0, 0.25, 118),

                    self._make_note(36, 2.0, 0.25, 110),

                ]

            else:

                pattern = [self._make_note(36, beat, 0.25, 126 if beat == 0 else 118) for beat in [0.0, 1.0, 2.0, 3.0]]

                if intensity >= 4 and genre in ['techno', 'tech-house']:

                    pattern.append(self._make_note(36, 3.5, 0.15, 94))

            notes = self._repeat_pattern(pattern, total_length, 4.0)

            if kind in ['build', 'drop', 'outro']:

                notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'clap':

            pitch = 38 if genre == 'drum-and-bass' else 39

            if kind == 'intro':

                pattern = [self._make_note(pitch, 2.75 if genre == 'reggaeton' else 3.0, 0.2, 88)]

            elif kind == 'break':

                pattern = [self._make_note(pitch, 1.0, 0.2, 84)]

            else:

                if genre == 'reggaeton':

                    # T115: Apply dembow groove template to snare/clap positions
                    if groove_template and groove_template.get('snare_positions'):
                        snare_positions = groove_template['snare_positions']
                        snare_velocities = groove_template.get('snare_velocities', [0.8] * len(snare_positions))
                        pattern = []
                        for i, pos in enumerate(snare_positions):
                            if pos < 4.0:  # Within one bar
                                vel = int(90 + (snare_velocities[i] * 30)) if i < len(snare_velocities) else 100
                                pattern.append(self._make_note(pitch, pos, 0.25, min(127, vel)))
                    else:
                        # Fallback to default dembow clap pattern
                        pattern = [

                            self._make_note(pitch, 1.0, 0.25, 108),

                            self._make_note(pitch, 2.75, 0.25, 112),

                        ]

                else:

                    pattern = [

                        self._make_note(pitch, 1.0, 0.25, 108),

                        self._make_note(pitch, 3.0, 0.25, 108),

                    ]

            notes = self._repeat_pattern(pattern, total_length, 4.0)

            if kind in ['build', 'drop']:

                notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'snare_fill':

            if kind not in ['build', 'break', 'drop']:

                return []

            if str(section.get('transition_fill', 'snare')).lower() not in ['snare', 'impact'] and kind != 'drop':

                return []

            fill_span = 2.0 if kind == 'build' and total_length >= 8.0 else 1.0

            fill_start = max(0.0, total_length - fill_span)

            step = 0.25 if intensity <= 2 else 0.125

            velocity = 76

            notes = []

            current = fill_start

            while current < total_length - 0.01:

                notes.append(self._make_note(38, current, 0.08 if step < 0.2 else 0.12, min(124, velocity)))

                current += step

                velocity += 3

            if kind == 'drop':

                notes.insert(0, self._make_note(38, 0.0, 0.15, 102))

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'hat_closed':

            if genre == 'reggaeton':

                # T115: Apply dembow groove template to hat positions
                if groove_template and groove_template.get('hat_positions'):
                    hat_positions = groove_template['hat_positions']
                    hat_velocities = groove_template.get('hat_velocities', [0.7] * len(hat_positions))
                    pattern = []
                    for i, pos in enumerate(hat_positions):
                        if pos < 4.0:  # Within one bar
                            vel = int(70 + (hat_velocities[i] * 30)) if i < len(hat_velocities) else 80
                            pattern.append(self._make_note(42, pos, 0.1, min(127, vel)))
                else:
                    # Fallback to default dembow hat pattern
                    pattern = [

                        self._make_note(42, 0.5, 0.1, 84),

                        self._make_note(42, 1.25, 0.08, 74),

                        self._make_note(42, 1.5, 0.1, 88),

                        self._make_note(42, 2.5, 0.1, 82),

                        self._make_note(42, 3.25, 0.08, 76),

                        self._make_note(42, 3.5, 0.1, 86),

                    ]

                if intensity >= 3:

                    # Add ghost hats at offbeat 8ths if not present
                    ghost_positions = [0.75, 2.75]
                    for ghost_pos in ghost_positions:
                        if not any(abs(n['start'] - ghost_pos) < 0.1 for n in pattern):
                            pattern.append(self._make_note(42, ghost_pos, 0.06, 62))

            elif intensity <= 1:

                pattern = [self._make_note(42, beat, 0.1, 86) for beat in [0.5, 1.5, 2.5, 3.5]]

            elif intensity == 2:

                pattern = [self._make_note(42, step * 0.5, 0.1, 90 if step % 2 == 0 else 72) for step in range(8)]

            else:

                pattern = [self._make_note(42, step * 0.5, 0.1, 92 if step % 2 == 0 else 74) for step in range(8)]

                pattern.extend([self._make_note(42, 1.75, 0.08, 64), self._make_note(42, 3.75, 0.08, 62)])

            notes = self._repeat_pattern(pattern, total_length, 4.0)

            if kind in ['build', 'drop', 'outro']:

                notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'hat_open':

            if kind in ['intro', 'break'] and intensity <= 1:

                return []

            if genre == 'reggaeton':

                # T115: Use groove template timing for open hats
                if groove_template and groove_template.get('hat_positions'):
                    hat_positions = groove_template['hat_positions']
                    # Use positions that are near offbeats (1.5-2.0 and 3.0-4.0)
                    offbeat_positions = [p for p in hat_positions if 1.5 <= p < 4.0 and p % 1.0 >= 0.5]
                    if len(offbeat_positions) >= 2:
                        pattern = [
                            self._make_note(46, offbeat_positions[0], 0.22, 72),
                            self._make_note(46, offbeat_positions[-1], 0.3, 84),
                        ]
                    else:
                        pattern = [self._make_note(46, 1.75, 0.22, 72), self._make_note(46, 3.5, 0.3, 84)]
                else:
                    pattern = [self._make_note(46, 1.75, 0.22, 72), self._make_note(46, 3.5, 0.3, 84)]

            else:

                pattern = [self._make_note(46, 3.5, 0.35, 82)]

            if intensity >= 3:

                pattern.append(self._make_note(46, 1.5, 0.25, 74))

            notes = self._repeat_pattern(pattern, total_length, 4.0)

            if kind in ['build', 'drop']:

                notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'top_loop':

            if kind in ['intro', 'break'] and intensity <= 1:

                return []

            pattern = [

                self._make_note(44, 0.25, 0.08, 56),

                self._make_note(44, 0.75, 0.08, 62),

                self._make_note(44, 1.25, 0.08, 58),

                self._make_note(44, 1.75, 0.08, 66),

                self._make_note(44, 2.25, 0.08, 58),

                self._make_note(44, 2.75, 0.08, 64),

                self._make_note(44, 3.25, 0.08, 60),

                self._make_note(44, 3.75, 0.08, 68),

            ]

            if genre == 'reggaeton' or 'latin' in style_text or 'dembow' in style_text or 'perreo' in style_text:

                # T115: Use groove template timing variance for latin percussion
                if groove_micro_offset > 0.0:
                    pattern.extend([
                        self._make_note(54, 0.5 + groove_micro_offset, 0.08, 52),
                        self._make_note(54, 2.5 - (groove_micro_offset * 0.5), 0.08, 54),
                    ])
                else:
                    pattern.extend([
                        self._make_note(54, 0.5, 0.08, 52),
                        self._make_note(54, 2.5, 0.08, 54),
                    ])

            if intensity >= 3:

                pattern.extend([

                    self._make_note(44, 1.125, 0.06, 48),

                    self._make_note(44, 3.125, 0.06, 50),

                ])

            return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)



        if role == 'perc':

            if kind in ['intro', 'outro'] and intensity <= 1:

                return []

            pattern = [

                self._make_note(37, 0.75, 0.1, 62),

                self._make_note(37, 1.25, 0.1, 58),

                self._make_note(37, 2.75, 0.1, 64),

                self._make_note(50, 3.25, 0.12, 70),

            ]

            if genre == 'reggaeton' or 'latin' in style_text or 'dembow' in style_text or 'perreo' in style_text:

                # T115: Use groove template positions for latin percussion accents
                if groove_template and groove_template.get('hat_positions'):
                    # Use hat positions as percussion accent timing
                    perc_positions = [p for p in groove_template['hat_positions'] if 1.0 <= p < 3.0]
                    if len(perc_positions) >= 2:
                        pattern.extend([
                            self._make_note(64, perc_positions[0], 0.12, 68),
                            self._make_note(64, perc_positions[1] if len(perc_positions) > 1 else perc_positions[0] + 0.5, 0.12, 64),
                        ])
                    else:
                        pattern.extend([

                            self._make_note(64, 1.75, 0.12, 68),

                            self._make_note(64, 2.125, 0.12, 64),

                        ])
                else:
                    pattern.extend([

                        self._make_note(64, 1.75, 0.12, 68),

                        self._make_note(64, 2.125, 0.12, 64),

                    ])

            if intensity >= 3:

                pattern.extend([self._make_note(37, 0.25, 0.1, 56), self._make_note(47, 2.25, 0.1, 68)])

            return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)



        if role == 'tom_fill':

            if kind not in ['build', 'drop']:

                return []

            if str(section.get('transition_fill', 'tom')).lower() not in ['tom', 'impact'] and kind != 'drop':

                return []

            fill_start = max(0.0, total_length - 1.0)

            sequence = [47, 50, 45, 47, 50]

            velocities = [72, 76, 80, 88, 96]

            notes = []

            for index, pitch in enumerate(sequence):

                start = fill_start + (index * 0.2)

                if start >= total_length:

                    break

                notes.append(self._make_note(pitch, start, 0.18, velocities[index]))

            return self._vary_drum_notes(notes, role, section, total_length)



        if role == 'ride':

            if kind not in ['build', 'drop', 'outro']:

                return []

            pattern = [self._make_note(51, float(beat), 0.2, 82) for beat in range(4)]

            if intensity >= 3:

                pattern.extend([self._make_note(51, beat + 0.5, 0.15, 64) for beat in range(4)])

            return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)



        if role == 'crash':

            if kind not in ['build', 'drop', 'break', 'outro']:

                return []

            hit_positions = [0.0]

            if kind == 'drop' and total_length >= 16.0:

                hit_positions.append(8.0)

            if kind == 'outro' and total_length >= 8.0:

                hit_positions.append(total_length - 4.0)

            notes = [

                self._make_note(49, position, min(1.5, max(0.25, total_length - position)), 82 if position == 0.0 else 70)

                for position in hit_positions

                if position < total_length

            ]

            return self._vary_drum_notes(notes, role, section, total_length)



        return []



    def _bass_style_for_section(self, genre: str, style: str, role: str, section_kind: str) -> str:

        style_text = f"{genre} {style}".lower()

        if role == 'sub_bass':

            return 'minimal' if section_kind != 'drop' else 'offbeat'

        if 'acid' in style_text:

            return 'acid'

        if genre == 'house':

            return 'offbeat'

        if genre == 'reggaeton':

            return 'minimal' if section_kind in ['intro', 'outro', 'break'] else 'offbeat'

        if genre == 'drum-and-bass':

            return 'rolling'

        if section_kind in ['intro', 'outro', 'break']:

            return 'minimal'

        if genre == 'tech-house':

            return 'offbeat'

        return 'rolling'



    def _render_bass_scene(self, role: str, genre: str, style: str, key: str,

                           section: Dict[str, Any]) -> List[Dict[str, Any]]:

        total_length = float(section['beats'])

        kind = section['kind']

        scale_name = 'minor' if 'm' in key.lower() else 'major'



        # Try to use musical theme if available

        theme_notes = self._get_theme_based_notes(role, section, total_length)

        if theme_notes is not None:

            notes = theme_notes

            self.logger.debug(f"[THEME] Using theme-based notes for {role} in {kind}")

        elif kind == 'break':

            notes = self._build_pad_motion(key, scale_name, total_length, 2, 4.0)

        else:

            notes = self.create_bassline(key, self._bass_style_for_section(genre, style, role, kind), total_length)



        if role == 'sub_bass' and theme_notes is None:

            notes = self._transpose_notes(notes, -12)

            notes = self._scale_note_lengths(notes, 1.35, minimum=0.2)

        notes = self._vary_bass_notes(notes, role, key, section, total_length)

        if kind in ['build', 'drop'] and total_length >= 8.0:

            turnaround = self._build_turnaround_notes(key, scale_name, total_length, 2 if role == 'bass' else 1, 88 if role == 'bass' else 80)

            notes = self._merge_section_notes(notes, turnaround, total_length)

        return notes



    def _render_musical_scene(self, role: str, genre: str, key: str, scale: str,
                              section: Dict[str, Any],
                              phrase_plan: Optional['PhrasePlan'] = None,
                              harmonic_hints: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
        """Render musical scene with optional hybrid materialization support.
        
        PRIORITY: Creates MIDI hook track first if harmonic hints are available,
        ensuring at least one MIDI harmonic track exists instead of only audio loops.
        
        Args:
            role: Track role ('chords', 'pad', 'pluck', 'arp', 'lead', 'counter')
            genre: Musical genre
            key: Musical key
            scale: Scale type
            section: Section configuration dict
            phrase_plan: Optional PhrasePlan for hybrid materialization
            harmonic_hints: Optional hints dict from reference analysis with 'pluck', 'piano', 'pad', 'keys'
            
        Returns:
            List of note dicts for the scene
        """
        total_length = float(section['beats'])
        kind = section['kind']
        
        # PRIORITY 1: Check if we have harmonic hints and haven't planned MIDI hook yet
        has_harmonic_hint = harmonic_hints and any(
            harmonic_hints.get(t) for t in ['pluck', 'piano', 'pad', 'keys']
        )
        has_phrase_harmonic_plan = bool(
            phrase_plan
            and getattr(phrase_plan, 'phrases', None)
            and any(
                PhrasePlan._normalize_family_name(getattr(phrase, 'family', None)) in {'pluck', 'piano', 'pad', 'keys'}
                or str(getattr(phrase, 'role', '') or '').strip().lower() in {'chords', 'pad', 'pluck', 'keys', 'piano', 'lead'}
                for phrase in list(getattr(phrase_plan, 'phrases', []) or [])
            )
        )
        
        # Log HARMONIC_GUIDE when hints are being used
        if has_harmonic_hint:
            available_tokens = [t for t in ['pluck', 'piano', 'pad', 'keys'] if harmonic_hints.get(t)]
            for token in available_tokens:
                hint_data = harmonic_hints.get(token, {})
                family = hint_data.get('family', 'unknown')
                self.logger.info(f"[HARMONIC_GUIDE] Using family {family} from reference (token: {token})")
        
        if not self._hook_planned and (has_harmonic_hint or has_phrase_harmonic_plan):
            # PRIORITY: Create MIDI hook track plan first
            hook_data = self._create_midi_hook_track(section, phrase_plan, harmonic_hints)
            if hook_data and hook_data.get('notes'):
                self.logger.info(f"[MIDI_HOOK_PRIORITY] Planned MIDI hook for {kind} "
                               f"before audio fallback ({len(hook_data['notes'])} notes)")
                # For pluck/piano roles, return the hook notes directly
                if role in ['pluck', 'piano', 'keys', 'lead']:
                    return hook_data['notes']
        
        # PRIORITY 2: Check if phrase plan has hints for this section
        if phrase_plan:
            phrase = next((p for p in phrase_plan.phrases 
                          if p.start <= section.get('start_bar', 0) < p.end 
                          and p.role == role), None)
            
            if phrase and phrase.instrument_hint:
                candidate = phrase.instrument_hint.get('primary_candidate', {})
                materialization_type = candidate.get('type', 'midi')
                
                if materialization_type == 'midi' and phrase.notes:
                    # Use phrase notes directly with mutation applied
                    self.logger.info(f"[HYBRID] Using MIDI phrase materialization for {role} in {kind}")
                    return phrase.notes
                elif materialization_type == 'preset':
                    # Store preset hint for later materialization
                    self.logger.info(f"[HYBRID] Using preset hint materialization for {role} in {kind}: {candidate.get('path', 'unknown')}")
                    # Return notes but mark for preset materialization
                    notes = phrase.notes if phrase.notes else self._render_musical_scene_fallback(role, genre, key, scale, section)
                    # Add metadata for server-side preset loading
                    for note in notes:
                        note['_preset_hint'] = candidate.get('path')
                        note['_preset_family'] = phrase.family
                    return notes
        
        # FALLBACK: Standard audio loop / MIDI generation
        return self._render_musical_scene_fallback(role, genre, key, scale, section)
    
    def _render_musical_scene_fallback(self, role: str, genre: str, key: str, scale: str,
                                       section: Dict[str, Any]) -> List[Dict[str, Any]]:
        """Original musical scene rendering (fallback)."""
        total_length = float(section['beats'])
        kind = section['kind']

        if role == 'pad':

            notes = self._build_pad_motion(key, scale, total_length, 4, 8.0 if kind == 'break' else 4.0)

            return self._vary_melodic_notes(notes, role, key, scale, section, total_length)



        if role == 'chords':

            # Try theme-based generation first

            theme_notes = self._get_theme_based_notes(role, section, total_length)

            if theme_notes is not None:

                self.logger.debug(f"[THEME] Using theme-based notes for {role} in {kind}")

                notes = theme_notes

            else:

                progression_type = 'techno' if genre in ['techno', 'tech-house'] else ('trance' if genre == 'trance' else 'house')

                notes = self.create_chord_progression(key, progression_type, total_length)

                notes = self._scale_note_lengths(notes, 1.15, minimum=0.25)

            return self._vary_melodic_notes(notes, role, key, scale, section, total_length)



        if role == 'stab':

            # Try theme-based generation first

            theme_notes = self._get_theme_based_notes('chords', section, total_length)

            if theme_notes is not None:

                notes = theme_notes

                notes = self._scale_note_lengths(notes, 0.4, minimum=0.1)

            else:

                notes = self.create_chord_progression(key, 'techno' if genre in ['techno', 'tech-house'] else 'house', total_length)

                notes = self._scale_note_lengths(notes, 0.4, minimum=0.1)

            shifted = []

            for note in notes:

                start = float(note['start']) + (0.5 if int(float(note['start'])) % 2 == 0 else 0.0)

                shifted.append(self._make_note(note['pitch'], start, note['duration'], min(118, note['velocity'] + 6)))

            return self._vary_melodic_notes(shifted, role, key, scale, section, total_length)



        if role == 'pluck':

            # Try theme-based generation first

            theme_notes = self._get_theme_based_notes(role, section, total_length)

            if theme_notes is not None:

                self.logger.debug(f"[THEME] Using theme-based notes for {role} in {kind}")

                notes = theme_notes

                notes = self._scale_note_lengths(notes, 0.55, minimum=0.12)

            else:

                notes = self.create_melody(key, scale, total_length, genre)

                notes = self._scale_note_lengths(notes, 0.55, minimum=0.12)

            return self._vary_melodic_notes(notes, role, key, scale, section, total_length)



        # Try theme-based generation for lead, arp, counter

        theme_notes = self._get_theme_based_notes(role, section, total_length)

        if theme_notes is not None:

            self.logger.debug(f"[THEME] Using theme-based notes for {role} in {kind}")

            notes = theme_notes

        else:

            notes = self.create_melody(key, scale, total_length, genre)

            if role == 'arp':

                notes = self._scale_note_lengths(notes, 0.45, minimum=0.1)

            elif role == 'lead':

                notes = self._transpose_notes(notes, 12)

            elif role == 'counter':

                sparse = []

                for note in notes:

                    start = float(note['start'])

                    if (start % 4.0) < 2.0:

                        continue

                    sparse.append(self._make_note(note['pitch'] - 12, start, max(0.2, float(note['duration']) * 0.8), max(50, int(note['velocity']) - 10)))

                notes = sparse

        notes = self._vary_melodic_notes(notes, role, key, scale, section, total_length)

        if role in ['lead', 'arp', 'pluck', 'counter'] and kind in ['build', 'drop'] and total_length >= 8.0:

            notes = self._merge_section_notes(notes, self._build_turnaround_notes(key, scale, total_length, 5, 84), total_length)

        return notes



    def _materialize_midi_phrase(self, phrase: 'Phrase', section: Dict[str, Any]) -> Dict[str, Any]:
        """Materialize a phrase as MIDI clip data.
        
        This method prepares the blueprint for MIDI phrase materialization.
        The actual Ableton track/clip creation happens in server.py.
        
        Args:
            phrase: The phrase to materialize
            section: Section configuration
            
        Returns:
            Dict with materialization info for server-side processing
        """
        clip_length = section.get('beats', 16.0)
        
        result = {
            'type': 'midi_phrase',
            'track_name': f"{phrase.family.upper()}_Phrase",
            'clip_name': f"{phrase.kind}_{phrase.section_kind}",
            'clip_length': clip_length,
            'notes': phrase.notes,
            'instrument_hint': phrase.instrument_hint,
            'family': phrase.family,
            'role': phrase.role,
            'mutation_type': phrase.mutation_type,
        }
        
        self.logger.info(f"[MATERIALIZE] MIDI phrase prepared: {result['track_name']} ({len(phrase.notes)} notes)")
        return result



    def _materialize_preset_phrase(self, phrase: 'Phrase', section: Dict[str, Any]) -> Dict[str, Any]:
        """Materialize a phrase with preset hint.
        
        Creates blueprint for MIDI track with preset hint.
        The actual Ableton device loading happens in server.py.
        
        Args:
            phrase: The phrase to materialize
            section: Section configuration
            
        Returns:
            Dict with materialization info for server-side processing
        """
        candidate = phrase.instrument_hint.get('primary_candidate', {}) if phrase.instrument_hint else {}
        preset_path = candidate.get('path', '')
        preset_name = preset_path.split('/')[-1] if preset_path else phrase.family
        
        clip_length = section.get('beats', 16.0)
        
        result = {
            'type': 'preset_hint',
            'track_name': f"Preset_{preset_name}",
            'clip_name': f"{phrase.kind}_{phrase.section_kind}",
            'clip_length': clip_length,
            'notes': phrase.notes,
            'preset_path': preset_path,
            'preset_family': phrase.family,
            'instrument_hint': phrase.instrument_hint,
            'role': phrase.role,
            'mutation_type': phrase.mutation_type,
        }
        
        self.logger.info(f"[MATERIALIZE] Preset hint prepared: {result['track_name']} -> {preset_path}")
        return result



    def _materialize_audio_loop(self, section: Dict[str, Any], role: str = 'music') -> Dict[str, Any]:
        """Materialize audio loop fallback.
        
        Creates blueprint for audio loop materialization.
        Used when MIDI/preset hints are not available.
        
        Args:
            section: Section configuration
            role: Audio role/loop type
            
        Returns:
            Dict with materialization info for server-side processing
        """
        result = {
            'type': 'audio_loop',
            'section_kind': section.get('kind', 'drop'),
            'section_name': section.get('name', 'Unknown'),
            'beats': section.get('beats', 16.0),
            'role': role,
            'fallback': True,
        }
        
        self.logger.info(f"[MATERIALIZE] Audio loop fallback: {section.get('kind', 'drop')} - {role}")
        return result

    def _create_midi_hook_track(self, section: Dict[str, Any], phrase_plan: Optional['PhrasePlan'], 
                                harmonic_hints: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
        """Create the mandatory MIDI harmonic hook track.
        
        This method creates a MIDI-based harmonic track using hints from reference analysis
        or phrase plan. It prioritizes MIDI materialization over audio loops.
        
        Args:
            section: Section configuration dict
            phrase_plan: Optional PhrasePlan with melodic phrases
            harmonic_hints: Optional hints from reference analysis containing 'pluck', 'piano', 'pad', 'keys'
            
        Returns:
            Dict with MIDI hook materialization info, or None if not possible
        """
        if isinstance(phrase_plan, dict):
            try:
                restored_sections = list(phrase_plan.get('sections', []) or [])
                phrase_plan = PhrasePlan.from_dict(
                    phrase_plan,
                    sections_override=restored_sections or [section],
                )
            except Exception as error:
                self.logger.warning("[MIDI_HOOK] Failed to restore phrase plan from dict: %s", error)
                phrase_plan = None

        # Determine which family to use based on priority
        priority = ['pluck', 'piano', 'pad', 'keys']
        selected_family = None
        selected_hint = None
        
        if harmonic_hints:
            for token in priority:
                if harmonic_hints.get(token):
                    selected_family = token
                    selected_hint = harmonic_hints[token]
                    break
        
        # If no hint from reference, try phrase plan
        if not selected_hint and phrase_plan:
            section_kind = section.get('kind', 'drop')
            phrases = phrase_plan.get_phrases_for_section(section_kind)
            
            # Look for phrases with harmonic instrument families
            for phrase in phrases:
                if phrase.family in priority:
                    selected_family = phrase.family
                    selected_hint = {
                        'family': phrase.family,
                        'primary_candidate': {
                            'item': {'name': f'{phrase.family}_Hook'},
                            'type': 'midi'
                        }
                    }
                    break
        
        if not selected_family:
            # Fallback: use pluck as default family
            selected_family = 'pluck'
            selected_hint = {
                'family': 'pluck',
                'primary_candidate': {
                    'item': {'name': 'Pluck_Hook'},
                    'type': 'midi'
                }
            }
        
        # Get phrase notes if available
        phrase_notes = []
        if phrase_plan:
            section_kind = section.get('kind', 'drop')
            phrases = phrase_plan.get_phrases_for_section(section_kind)
            if phrases:
                # Use the first phrase's notes
                phrase_notes = phrases[0].notes
        
        # Generate hook notes if no phrase notes available
        if not phrase_notes:
            phrase_notes = self._generate_hook_notes(section, selected_family)

        arrangement_notes = []
        arrangement_length_beats = float(section.get('beats', 16.0) or 16.0)
        if phrase_plan and getattr(phrase_plan, 'phrases', None):
            selected_phrases_by_start: Dict[float, Tuple[int, Any]] = {}
            harmonic_roles = {'pluck', 'piano', 'keys', 'pad', 'lead', 'synth'}

            for phrase in phrase_plan.phrases:
                phrase_family = PhrasePlan._normalize_family_name(getattr(phrase, 'family', None))
                phrase_role = str(getattr(phrase, 'role', '') or '').strip().lower()
                phrase_notes_data = list(getattr(phrase, 'notes', []) or [])
                if not phrase_notes_data:
                    continue

                score = 0
                if phrase_family == selected_family:
                    score = 3
                elif selected_family in {'piano', 'keys'} and phrase_family in {'piano', 'keys'}:
                    score = 2
                elif phrase_role in harmonic_roles:
                    score = 1

                if score <= 0:
                    continue

                phrase_start = float(getattr(phrase, 'start', 0.0) or 0.0)
                current = selected_phrases_by_start.get(phrase_start)
                if current is None or score > current[0]:
                    selected_phrases_by_start[phrase_start] = (score, phrase)

            for phrase_start in sorted(selected_phrases_by_start.keys()):
                phrase = selected_phrases_by_start[phrase_start][1]
                phrase_end = float(getattr(phrase, 'end', phrase_start + 16.0) or (phrase_start + 16.0))
                arrangement_length_beats = max(arrangement_length_beats, phrase_end)
                for note in list(getattr(phrase, 'notes', []) or []):
                    note_start = float(note.get('start_time', note.get('start', 0.0)) or 0.0)
                    arrangement_notes.append({
                        'pitch': int(note.get('pitch', 60)),
                        'start': round(phrase_start + note_start, 6),
                        'duration': float(note.get('duration', 0.25) or 0.25),
                        'velocity': int(note.get('velocity', 100) or 100),
                        'mute': bool(note.get('mute', False)),
                    })

        if not arrangement_notes:
            arrangement_notes = [
                {
                    'pitch': int(note.get('pitch', 60)),
                    'start': float(note.get('start_time', note.get('start', 0.0)) or 0.0),
                    'duration': float(note.get('duration', 0.25) or 0.25),
                    'velocity': int(note.get('velocity', 100) or 100),
                    'mute': bool(note.get('mute', False)),
                }
                for note in phrase_notes
            ]

        # Build the MIDI hook track data
        # P0 Sprint v0.1.29: HARMONY_PIANO_MIDI as the harmonic spine
        track_name = "HARMONY_PIANO_MIDI"
        
        hook_data = {
            'type': 'midi_hook',
            'track_name': track_name,
            'family': selected_family,
            'candidate': selected_hint.get('primary_candidate', {}),
            'notes': phrase_notes,
            'arrangement_notes': arrangement_notes,
            'arrangement_length_beats': arrangement_length_beats,
            'section': section.get('kind', 'drop'),
            'section_length_beats': section.get('beats', 16.0),
            'mandatory': True,
            'instrument_hint': selected_hint
        }
        
        # Mark as PLANNED (not materialized yet!)
        self._hook_planned = True
        self._hook_planned_data = hook_data
        # Legacy compatibility
        self._midi_hook_created = True
        self._midi_hook_data = hook_data
        
        self.logger.info(f"[HOOK_PLANNED] Created mandatory MIDI hook track plan: {track_name} "
                        f"({len(phrase_notes)} notes, family={selected_family}) - NOT materialized yet!")
        
        return hook_data
    
    def mark_hook_materialized(self, track_idx: int) -> None:
        """Mark that hook was actually created in Ableton Live.
        
        Args:
            track_idx: The actual track index in Ableton Live
        """
        self._hook_materialized = True
        self._hook_materialized_idx = track_idx
        self.logger.info(f"[HOOK_MATERIALIZED] MIDI hook materialized at track index {track_idx}")
    
    def get_hook_plan(self) -> Optional[Dict[str, Any]]:
        """Get the planned hook data if available.
        
        Returns:
            Hook data dict if hook was planned, None otherwise
        """
        return self._hook_planned_data if self._hook_planned else None
    
    def _generate_hook_notes(self, section: Dict[str, Any], family: str) -> List[Dict[str, Any]]:
        """Generate default hook notes for a section.
        
        Args:
            section: Section configuration
            family: Instrument family ('pluck', 'piano', 'pad', 'keys')
            
        Returns:
            List of note dicts for the hook
        """
        section_kind = section.get('kind', 'drop')
        total_length = section.get('beats', 16.0)
        
        # Use musical theme if available
        if self.musical_theme:
            motif = self.musical_theme.get_section_variation(section_kind)
            
            if family in ['pluck', 'keys']:
                notes = self.musical_theme.motif_to_lead(motif, embellishment_level=0.5)
            elif family == 'piano':
                chords = self.musical_theme.motif_to_chords(motif, voicing='triad')
                notes = []
                for chord in chords:
                    for pitch in chord['notes']:
                        notes.append({
                            'pitch': pitch,
                            'start': chord['start'],
                            'duration': chord['duration'],
                            'velocity': chord['velocity']
                        })
            elif family == 'pad':
                notes = self.musical_theme.motif_to_bass(motif, octave_offset=0)
                # Extend durations for pad feel
                for note in notes:
                    note['duration'] = min(note['duration'] * 2, 4.0)
                    note['velocity'] = min(note['velocity'], 80)
            else:
                notes = motif
            
            return notes
        
        # Fallback: generate simple hook based on section kind
        notes = []
        key = 'Am'  # Default
        scale = 'minor'
        
        if section_kind == 'drop':
            # Active hook for drop - quarter notes on root
            root_midi = self.note_name_to_midi('A', 4)
            for i in range(int(total_length)):
                notes.append({
                    'pitch': root_midi + (i % 7),  # Scale degrees
                    'start': float(i),
                    'duration': 0.5,
                    'velocity': 100
                })
        elif section_kind == 'build':
            # Tension building - rising line
            root_midi = self.note_name_to_midi('A', 4)
            for i in range(0, int(total_length), 2):
                notes.append({
                    'pitch': root_midi + (i // 2),
                    'start': float(i),
                    'duration': 1.0,
                    'velocity': 80 + (i // 2) * 5
                })
        else:
            # Sparse for intro/break/outro
            root_midi = self.note_name_to_midi('A', 4)
            for i in range(0, int(total_length), 4):
                notes.append({
                    'pitch': root_midi,
                    'start': float(i),
                    'duration': 2.0,
                    'velocity': 70
                })
        
        return notes
    
    def _get_phrase_for_section(self, phrase_plan: 'PhrasePlan', section_kind: str) -> Optional['Phrase']:
        """Get the primary phrase for a section kind from a phrase plan."""
        phrases = phrase_plan.get_phrases_for_section(section_kind)
        return phrases[0] if phrases else None

    def _render_fx_scene(self, role: str, key: str, section: Dict[str, Any]) -> List[Dict[str, Any]]:

        total_length = float(section['beats'])

        kind = section.get('kind', 'drop')

        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, 5)

        rng = self._section_rng(section, role, salt=19)



        if role == 'drone':

            notes = [

                self._make_note(root_midi - 12, 0.0, min(total_length, 8.0 if kind == 'break' else total_length), 58),

                self._make_note(root_midi - 5, max(0.0, total_length / 2.0), min(total_length / 2.0, 8.0), 52),

            ]

            if kind in ['build', 'drop'] and total_length >= 12.0:

                notes.append(self._make_note(root_midi + 2, max(0.0, total_length - 6.0), 4.0, 48))

            return notes



        if role == 'reverse_fx':

            if str(section.get('transition_fill', 'reverse')).lower() not in ['reverse', 'impact'] and kind not in ['break', 'build']:

                return []

            notes = []

            for span, offset, velocity in ((4.0, 4.0, 70), (2.0, 2.0, 64), (1.0, 1.0, 58)):

                if total_length >= offset:

                    start = max(0.0, total_length - offset)

                    notes.append(self._make_note(root_midi + 12, start, min(span, total_length - start), velocity))

            if kind == 'build' and total_length >= 16.0 and rng.random() > 0.35:

                notes.append(self._make_note(root_midi + 7, max(0.0, total_length - 8.0), 1.5, 56))

            return notes



        if role == 'riser':

            notes = []

            sweep_start = max(0.0, total_length - min(8.0, total_length))

            for offset, pitch, velocity in ((0.0, root_midi + 7, 64), (2.0, root_midi + 12, 70), (4.0, root_midi + 19, 74), (6.0, root_midi + 24, 78)):

                start = sweep_start + offset

                if start < total_length:

                    notes.append(self._make_note(pitch, start, min(2.0, total_length - start), velocity))

            if kind == 'build' and total_length >= 8.0:

                notes.extend([

                    self._make_note(root_midi + 12, max(0.0, total_length - 2.0), 0.5, 82),

                    self._make_note(root_midi + 19, max(0.0, total_length - 1.0), 0.45, 86),

                ])

            return notes



        if role == 'impact':

            if kind in ['intro', 'outro'] and str(section.get('transition_fill', 'impact')).lower() != 'impact':

                return []

            notes = [self._make_note(root_midi + 7, 0.0, 0.5, 82)]

            if total_length >= 8.0 and kind in ['build', 'drop']:

                notes.append(self._make_note(root_midi + 12, total_length - 0.5, 0.45, 76))

            if kind == 'drop' and total_length >= 16.0 and rng.random() > 0.4:

                notes.append(self._make_note(root_midi + 10, 8.0, 0.35, 72))

            return notes



        if role == 'atmos':

            notes = [

                self._make_note(root_midi, 0.0, min(8.0, total_length), 54),

                self._make_note(root_midi + 7, max(0.0, total_length / 2.0), min(8.0, total_length / 2.0), 50),

            ]

            if kind in ['intro', 'break', 'outro'] and total_length >= 12.0:

                notes.append(self._make_note(root_midi + 12, max(0.0, total_length - 4.0), min(4.0, total_length), 46))

            return notes



        if role == 'vocal':

            notes = []

            if kind == 'intro':

                base_positions = [7.5, 15.5]

            elif kind == 'build':

                base_positions = [1.5, 3.5, 5.5, 7.5]

                if total_length >= 16.0:

                    base_positions.extend([11.5, 13.5, 15.5])

            elif kind == 'drop':

                base_positions = [1.5, 2.75, 5.5, 6.75]

                if total_length >= 16.0:

                    base_positions.extend([9.5, 10.75, 13.5, 14.75])

            elif kind == 'break':

                base_positions = [3.5, 11.5]

            else:

                base_positions = [1.5, 5.5]



            for index, pos in enumerate(base_positions):

                if pos >= total_length:

                    continue

                pitch = root_midi + (10 if kind == 'drop' and index % 2 else 3)

                duration = 0.22 if kind == 'drop' else 0.3

                velocity = 80 if kind in ['build', 'drop'] else 72

                if rng.random() > 0.82:

                    pitch += 12

                notes.append(self._make_note(pitch, pos, duration, velocity))



            if kind == 'build' and total_length >= 8.0:

                notes.append(self._make_note(root_midi + 15, max(0.0, total_length - 0.75), 0.22, 84))

            return notes



        return []



    def _build_pad_motion(self, key: str, scale_name: str, total_length: float,

                          octave: int = 4, sustain_beats: float = 4.0) -> List[Dict[str, Any]]:

        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, octave)

        scale_notes = self.get_scale_notes(root_midi, scale_name)

        progression = random.choice(CHORD_PROGRESSIONS.get('techno' if 'm' in key.lower() else 'house', CHORD_PROGRESSIONS['techno']))

        notes = []

        bars = max(1, int(total_length / 4.0))



        for bar in range(bars):

            degree = progression[bar % len(progression)] - 1

            chord_root = scale_notes[degree % len(scale_notes)]

            start = float(bar * 4.0)

            duration = min(sustain_beats, total_length - start)

            for interval in [0, 7, 12]:

                notes.append(self._make_note(chord_root + interval, start, duration, 66))

        return notes



    def _generate_drum_tracks(self, genre: str, style: str) -> List[Dict]:

        """Genera tracks de baterÃ­a"""

        tracks = []



        # Kick siempre

        tracks.append({

            'name': 'Kick',

            'type': 'midi',

            'color': TRACK_COLORS['kick'],

            'clip': {

                'slot': 0,

                'length': 4.0,

                'notes': self._create_kick_pattern(genre, style)

            }

        })



        # Snare/Clap

        tracks.append({

            'name': 'Clap',

            'type': 'midi',

            'color': TRACK_COLORS['clap'],

            'clip': {

                'slot': 0,

                'length': 4.0,

                'notes': self._create_clap_pattern(genre, style)

            }

        })



        # Hi-hats

        tracks.append({

            'name': 'HiHat',

            'type': 'midi',

            'color': TRACK_COLORS['hat'],

            'clip': {

                'slot': 0,

                'length': 4.0,

                'notes': self._create_hat_pattern(genre, style)

            }

        })



        # PercusiÃ³n extra para estilos mÃ¡s complejos

        if style in ['latin', 'afro', 'groovy', 'complex']:

            tracks.append({

                'name': 'Percussion',

                'type': 'midi',

                'color': TRACK_COLORS['hat'],

                'clip': {

                    'slot': 0,

                    'length': 4.0,

                    'notes': self._create_perc_pattern(genre, style)

                }

            })



        return tracks



    def _generate_bass_track(self, key: str, scale: str, genre: str, style: str) -> Dict:

        """Genera un track de bajo"""

        notes = self.create_bassline(key, style, 16.0)



        return {

            'name': 'Bass',

            'type': 'midi',

            'color': TRACK_COLORS['bass'],

            'clip': {

                'slot': 0,

                'length': 16.0,

                'notes': notes

            }

        }



    def _generate_chord_track(self, key: str, scale: str, genre: str) -> Dict:

        """Genera un track de acordes"""

        notes = self.create_chord_progression(key, genre, 16.0)



        return {

            'name': 'Chords',

            'type': 'midi',

            'color': TRACK_COLORS['chords'],

            'clip': {

                'slot': 0,

                'length': 16.0,

                'notes': notes

            }

        }



    def _generate_lead_track(self, key: str, scale: str, genre: str) -> Dict:

        """Genera un track lead/melÃ³dico"""

        notes = self.create_melody(key, scale, 16.0, genre)



        return {

            'name': 'Lead',

            'type': 'midi',

            'color': TRACK_COLORS['synth'],

            'clip': {

                'slot': 0,

                'length': 16.0,

                'notes': notes

            }

        }



    def _generate_fx_track(self) -> Dict:

        """Genera un track de FX/AtmÃ³sfera"""

        return {

            'name': 'FX',

            'type': 'midi',

            'color': TRACK_COLORS['fx'],

            'clip': {

                'slot': 0,

                'length': 16.0,

                'notes': self._create_fx_notes()

            }

        }



    # =========================================================================

    # PATRONES DE BATERÃA

    # =========================================================================



    def _create_kick_pattern(self, genre: str, style: str) -> List[Dict]:

        """Crea patrÃ³n de kick"""

        notes = []



        if style == 'minimal':

            # Kick en 1 y 2.5

            for bar in range(4):

                notes.append({'pitch': 36, 'start': bar * 4.0, 'duration': 0.25, 'velocity': 120})

                notes.append({'pitch': 36, 'start': bar * 4.0 + 2.5, 'duration': 0.25, 'velocity': 110})

        elif style == 'four-on-the-floor' or genre in ['house', 'tech-house']:

            # 4/4 clÃ¡sico

            for bar in range(4):

                for beat in range(4):

                    notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': 127})

        elif genre in ['reggaeton', 'perreo'] or style in ['dembow', 'dembow_95bpm', 'dembow_hard']:

            # PatrÃ³n Dembow: Kick en 1, 8, 10, 13 (grilla de 16 corcheas = 1 barra de 4/4)

            # En beats: 0, 1.75, 2.25, 3.0

            for bar in range(4):

                kick_positions = [0.0, 1.75, 2.25, 3.0]

                for pos in kick_positions:

                    vel = 127 if pos == 0.0 else 115

                    notes.append({'pitch': 36, 'start': bar * 4.0 + pos, 'duration': 0.25, 'velocity': vel})

        else:  # Default techno

            for bar in range(4):

                for beat in range(4):

                    vel = 127 if beat == 0 else 115

                    notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': vel})



        return notes



    def _create_clap_pattern(self, genre: str, style: str) -> List[Dict]:

        """Crea patrÃ³n de clap/snare"""

        notes = []



        if genre in ['reggaeton', 'perreo'] or style in ['dembow', 'dembow_95bpm', 'dembow_hard']:

            # PatrÃ³n Dembow: Snare en posiciÃ³n 5 (grilla de 16 corcheas) = beat 1.0

            for bar in range(4):

                notes.append({'pitch': 40, 'start': bar * 4.0 + 1.0, 'duration': 0.25, 'velocity': 110})

        else:

            # Claps en 2 y 4 (beats 1 y 3 en 0-indexed)

            for bar in range(4):

                notes.append({'pitch': 40, 'start': bar * 4.0 + 1.0, 'duration': 0.25, 'velocity': 110})

                notes.append({'pitch': 40, 'start': bar * 4.0 + 3.0, 'duration': 0.25, 'velocity': 110})



        # Snare adicional para DnB/Jungle

        if genre == 'drum-and-bass':



            for bar in range(4):

                notes.append({'pitch': 38, 'start': bar * 4.0 + 1.75, 'duration': 0.1, 'velocity': 90})

                notes.append({'pitch': 38, 'start': bar * 4.0 + 2.25, 'duration': 0.1, 'velocity': 85})



        return notes



    def _create_hat_pattern(self, genre: str, style: str) -> List[Dict]:

        """Crea patrÃ³n de hi-hats"""

        notes = []



        if genre in ['reggaeton', 'perreo'] or style in ['dembow', 'dembow_95bpm', 'dembow_hard']:

            # PatrÃ³n Dembow: Hats en cada corchea par (0, 0.5, 1, 1.5, 2, 2.5, 3, 3.5)

            for bar in range(4):

                for beat in range(4):

                    # Corchea par = 0, corchea impar = 0.5

                    time = bar * 4.0 + beat

                    vel = 95 if beat % 2 == 0 else 75

                    notes.append({'pitch': 42, 'start': time, 'duration': 0.1, 'velocity': vel})

                    # Hat en la mitad del beat

                    notes.append({'pitch': 42, 'start': time + 0.5, 'duration': 0.1, 'velocity': 70})



        elif style in ['minimal', 'dub']:

            # Off-bats simples

            for bar in range(4):

                for beat in range(4):

                    notes.append({'pitch': 42, 'start': bar * 4.0 + beat + 0.5, 'duration': 0.1, 'velocity': 90})

        else:

            # 8vos con variaciÃ³n

            for bar in range(4):

                for beat in range(4):

                    for sub in range(2):

                        time = bar * 4.0 + beat + sub * 0.5

                        vel = 90 if sub == 0 else 70

                        notes.append({'pitch': 42, 'start': time, 'duration': 0.1, 'velocity': vel})



            # Open hats ocasionales

            if style not in ['minimal']:

                for bar in range(4):

                    notes.append({'pitch': 46, 'start': bar * 4.0 + 3.5, 'duration': 0.5, 'velocity': 80})



        return notes



    def _create_perc_pattern(self, genre: str, style: str) -> List[Dict]:

        """Crea patrÃ³n de percusiÃ³n extra"""

        notes = []



        for bar in range(4):

            # Shakers/congas en 16vos

            for i in range(16):

                time = bar * 4.0 + i * 0.25

                if i % 4 != 0:  # Skip downbeats

                    vel = 60 + random.randint(-10, 10)

                    notes.append({'pitch': 37, 'start': time, 'duration': 0.1, 'velocity': vel})



        return notes



    def _create_fx_notes(self) -> List[Dict]:

        """Crea notas para FX/atmÃ³sfera"""

        notes = []



        # Swells y risers

        for bar in [0, 2]:

            # Nota larga ascendente

            notes.append({'pitch': 84, 'start': bar * 4.0 + 3.0, 'duration': 1.0, 'velocity': 70})



        return notes



    # =========================================================================

    # CREACIÃ“N DE PATRONES PARA MCP

    # =========================================================================



    def create_drum_pattern(self, style: str, pattern_type: str, length: float) -> List[Dict]:

        """Crea un patrÃ³n de baterÃ­a completo para usar con MCP"""

        notes = []

        bars = int(length / 4.0)



        if pattern_type == 'kick-only':

            for bar in range(bars):

                for beat in range(4):

                    notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': 127})



        elif pattern_type == 'hats-only':

            for bar in range(bars):

                for beat in range(4):

                    notes.append({'pitch': 42, 'start': bar * 4.0 + beat + 0.5, 'duration': 0.1, 'velocity': 90})



        elif pattern_type == 'minimal':

            for bar in range(bars):

                notes.append({'pitch': 36, 'start': bar * 4.0, 'duration': 0.25, 'velocity': 127})

                notes.append({'pitch': 40, 'start': bar * 4.0 + 2.0, 'duration': 0.25, 'velocity': 110})

                notes.append({'pitch': 42, 'start': bar * 4.0 + 2.5, 'duration': 0.1, 'velocity': 80})



        else:  # full

            notes.extend(self._create_kick_pattern(style, 'standard'))

            notes.extend(self._create_clap_pattern(style, 'standard'))

            notes.extend(self._create_hat_pattern(style, 'standard'))



        return notes



    def create_bassline(self, key: str, style: str, length: float) -> List[Dict]:

        """Crea una lÃ­nea de bajo musical"""

        notes = []



        # Parsear key

        root_note = key[:-1] if len(key) > 1 else key

        is_minor = 'm' in key.lower()

        scale_name = 'minor' if is_minor else 'major'



        root_midi = self.note_name_to_midi(root_note, 2)  # Octava 2 para bajo

        scale_notes = self.get_scale_notes(root_midi, scale_name)



        bars = int(length / 4.0)



        if style == 'rolling':

            # Bass en 16vos

            for bar in range(bars):

                for beat in range(4):

                    for sub in range(4):

                        time = bar * 4.0 + beat + sub * 0.25

                        if sub == 0:

                            pitch = root_midi

                            vel = 120

                        elif sub == 2:

                            pitch = scale_notes[4] if len(scale_notes) > 4 else root_midi + 7

                            vel = 100

                        else:

                            pitch = root_midi

                            vel = 80 if sub % 2 == 0 else 70



                        notes.append({'pitch': pitch, 'start': time, 'duration': 0.2, 'velocity': vel})



        elif style == 'minimal':

            # Solo en beats 1 y 3

            for bar in range(bars):

                for beat in [0, 2]:

                    time = bar * 4.0 + beat

                    notes.append({'pitch': root_midi, 'start': time, 'duration': 1.5, 'velocity': 110})



        elif style == 'offbeat':

            # Notas en off-beats (house tÃ­pico)

            for bar in range(bars):

                for beat in range(4):

                    time = bar * 4.0 + beat + 0.5

                    pitch = root_midi if beat % 2 == 0 else scale_notes[3]

                    notes.append({'pitch': pitch, 'start': time, 'duration': 0.4, 'velocity': 100})



        elif style == 'acid':

            # Estilo TB-303 con slides

            for bar in range(bars):

                for i in range(8):

                    time = bar * 4.0 + i * 0.5

                    pitch = root_midi + random.choice([0, 3, 5, 7, 10])

                    vel = 90 + random.randint(-20, 20)

                    notes.append({'pitch': pitch, 'start': time, 'duration': 0.4, 'velocity': min(127, max(60, vel))})



        elif style == 'dembow' or style == 'bouncy':

            # Bass line dembow bouncy con tumbao: nota en el 1, silencio, nota sincopada corta en el 2-y, nota en el 3

            for bar in range(bars):

                # Positions: [0, 0.5, 1.5, 2, 2.5, 3] dentro de la barra

                positions = [0.0, 0.5, 1.5, 2.0, 2.5, 3.0]

                velocities = [110, 80, 90, 110, 85, 95]

                durations = [0.4, 0.2, 0.3, 0.4, 0.2, 0.35]

                for i, pos in enumerate(positions):

                    time = bar * 4.0 + pos

                    if i == 0 or i == 3:

                        pitch = root_midi

                    elif i == 2:

                        pitch = scale_notes[4] if len(scale_notes) > 4 else root_midi + 7

                    else:

                        pitch = root_midi + 5 if len(scale_notes) > 5 else root_midi

                    notes.append({'pitch': pitch, 'start': time, 'duration': durations[i], 'velocity': velocities[i]})



        elif style == 'reese_reggaeton':

            # Bajo Reese distorsionado y subterráneo para reggaeton (octava 1-2, duración más larga)

            sub_root = self.note_name_to_midi(root_note, 1)  # Octava 1 para sub bass

            for bar in range(bars):

                # Sustained sub bass with slight movement

                time = bar * 4.0

                # Long sustained notes with occasional pitch dips

                notes.append({'pitch': sub_root, 'start': time, 'duration': 3.8, 'velocity': 100})

                # Add subtle sub harmonics for Reese character

                if bar % 2 == 0:

                    notes.append({'pitch': sub_root + 12, 'start': time + 0.1, 'duration': 3.6, 'velocity': 60})



        else:  # walking

            for bar in range(bars):

                for beat in range(4):

                    time = bar * 4.0 + beat

                    if beat == 0:

                        pitch = root_midi

                    elif beat == 1:

                        pitch = scale_notes[2] if len(scale_notes) > 2 else root_midi + 3

                    elif beat == 2:

                        pitch = scale_notes[3] if len(scale_notes) > 3 else root_midi + 5

                    else:

                        pitch = scale_notes[4] if len(scale_notes) > 4 else root_midi + 7



                    notes.append({'pitch': pitch, 'start': time, 'duration': 0.9, 'velocity': 100})



        return notes



    def create_chord_progression(self, key: str, progression_type: str, length: float) -> List[Dict]:

        """Crea una progresiÃ³n de acordes"""

        notes = []



        # Parsear key

        root_note = key[:-1] if len(key) > 1 else key

        is_minor = 'm' in key.lower()

        scale_name = 'minor' if is_minor else 'major'



        root_midi = self.note_name_to_midi(root_note, 4)  # Octava 4 para acordes

        scale_notes = self.get_scale_notes(root_midi, scale_name)



        # Seleccionar progresiÃ³n

        progressions = CHORD_PROGRESSIONS.get(progression_type, CHORD_PROGRESSIONS['techno'])

        progression = random.choice(progressions)



        bars = int(length / 4.0)

        beats_per_bar = 4



        for bar in range(bars):

            degree = progression[bar % len(progression)] - 1



            if degree < len(scale_notes):

                chord_root = scale_notes[degree]

            else:

                chord_root = root_midi



            # Construir acorde (triada)

            third = 3 if 'minor' in scale_name else 4

            chord_tones = [chord_root, chord_root + third, chord_root + 7]



            # Stab chords - cortos y percusivos

            if progression_type == 'techno':

                for pitch in chord_tones:

                    notes.append({

                        'pitch': pitch,

                        'start': bar * beats_per_bar,

                        'duration': 0.25,

                        'velocity': 90

                    })

            elif progression_type == 'house':

                for beat in [0.5, 2.5]:

                    for pitch in chord_tones:

                        notes.append({

                            'pitch': pitch,

                            'start': bar * beats_per_bar + beat,

                            'duration': 0.5,

                            'velocity': 75

                        })

            else:

                # Default: acordes en beats 1 y 3

                for beat in [0, 2]:

                    for pitch in chord_tones:

                        notes.append({

                            'pitch': pitch,

                            'start': bar * beats_per_bar + beat,

                            'duration': 1.0,

                            'velocity': 85

                        })



        return notes



    def create_melody(self, key: str, scale: str, length: float, genre: str) -> List[Dict]:

        """Crea una melodÃ­a/lead"""

        notes = []



        root_note = key[:-1] if len(key) > 1 else key

        root_midi = self.note_name_to_midi(root_note, 5)  # Octava 5 para lead

        scale_notes = self.get_scale_notes(root_midi, scale)



        bars = max(1, int(length / 4.0))

        motif_pool = [

            ([0, 2, 4, 2, 5, 4], [0.0, 0.5, 1.5, 2.0, 2.75, 3.25]),

            ([0, 3, 4, 6, 4], [0.0, 0.75, 1.5, 2.5, 3.25]),

            ([0, 2, 3, 5, 3, 2], [0.0, 0.5, 1.0, 2.0, 2.5, 3.5]),

        ]

        motif_steps, motif_times = random.choice(motif_pool)



        for bar in range(bars):

            bar_offset = bar * 4.0

            phrase_shift = 0 if bar % 4 in [0, 1] else random.choice([0, 1, -1, 2])

            invert_tail = (bar % 4 == 3)

            for index, step in enumerate(motif_steps):

                start = bar_offset + motif_times[index % len(motif_times)]

                if start >= length:

                    continue

                if invert_tail and index >= max(1, len(motif_steps) - 2):

                    start += 0.25

                if random.random() < 0.18 and index not in [0, len(motif_steps) - 1]:

                    continue



                scale_index = (step + phrase_shift) % len(scale_notes)

                pitch = scale_notes[scale_index]

                if genre in ['trance', 'progressive'] and index == len(motif_steps) - 1:

                    pitch += 12

                elif genre in ['techno', 'tech-house'] and index % 3 == 2:

                    pitch -= 12



                duration = 0.22 if start % 1.0 not in [0.0, 0.5] else 0.35

                velocity = 78 + ((index + bar) % 3) * 8 + random.randint(-6, 8)

                notes.append({

                    'pitch': pitch,

                    'start': start,

                    'duration': duration,

                    'velocity': max(60, min(123, velocity))

                })



        return notes

