""" song_generator.py - Generador musical para AbletonMCP-AI. """ 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 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 ], } # 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'], }, } # 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), ], } 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), ], ], } 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', }, }, ) 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() # ============================================================================= # 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' }, } # Pattern variant diversity memory - track used variants across generations _pattern_variant_memory: Dict[str, Dict[str, int]] = { 'drum': {}, 'bass': {}, 'melodic': {}, } def _get_pattern_variant_penalty(category: str, variant: str) -> float: """Get penalty for a pattern variant based on cross-generation usage.""" if variant in _pattern_variant_memory.get(category, {}): count = _pattern_variant_memory[category].get(variant, 0) return min(0.4, count * 0.08) return 0.0 def _record_pattern_variant_usage(category: str, variant: str) -> None: """Record that a pattern variant was used.""" if category not in _pattern_variant_memory: _pattern_variant_memory[category] = {} _pattern_variant_memory[category][variant] = _pattern_variant_memory[category].get(variant, 0) + 1 def _decay_pattern_variant_memory() -> None: """Decay pattern variant memory to allow reuse after generations.""" for category in _pattern_variant_memory: for variant in list(_pattern_variant_memory[category].keys()): _pattern_variant_memory[category][variant] = max(0, _pattern_variant_memory[category][variant] - 1) if _pattern_variant_memory[category][variant] <= 0: del _pattern_variant_memory[category][variant] def reset_pattern_variant_memory() -> None: """Reset all pattern variant memory.""" global _pattern_variant_memory _pattern_variant_memory = {'drum': {}, 'bass': {}, 'melodic': {}} # 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 HumanFeelEngine: """ T040-T050: Engine de humanizacion y dinamica. Aplica variaciones de timing, velocity y groove a patrones MIDI. """ def __init__(self, seed: int = 42): self.rng = random.Random(seed) self._groove_templates = { 'straight': {'swing': 0.0, 'humanize': 0.0}, 'shuffle': {'swing': 0.33, 'humanize': 0.02}, 'triplet': {'swing': 0.66, 'humanize': 0.03}, 'latin': {'swing': 0.25, 'humanize': 0.04}, } def apply_timing_variation(self, notes: List[Dict], amount_ms: float = 5.0) -> List[Dict]: """T040: Micro-offsets de timing (-5ms a +5ms).""" result = [] for note in notes: offset = self.rng.uniform(-amount_ms, amount_ms) / 1000.0 # Convert to seconds new_note = dict(note) new_note['start'] = note.get('start', 0) + offset result.append(new_note) return result def apply_velocity_humanize(self, notes: List[Dict], variance: float = 0.05) -> List[Dict]: """T041: Humanizacion de velocity (+-5% variacion).""" result = [] for note in notes: vel = note.get('velocity', 100) variation = self.rng.uniform(-variance, variance) new_vel = int(vel * (1 + variation)) new_vel = max(1, min(127, new_vel)) # Clamp to MIDI range new_note = dict(note) new_note['velocity'] = new_vel result.append(new_note) return result def apply_note_skip_probability(self, notes: List[Dict], prob: float = 0.02) -> List[Dict]: """T042: Probabilidad de skip nota (2% ghost notes).""" result = [] for note in notes: if self.rng.random() > prob: # Keep note with probability (1-prob) result.append(note) return result def apply_groove(self, notes: List[Dict], style: str = 'shuffle', amount: float = 0.5) -> List[Dict]: """T044-T046: Aplica groove template.""" template = self._groove_templates.get(style, self._groove_templates['straight']) swing = template['swing'] * amount result = [] for note in notes: start = note.get('start', 0) # Apply swing to off-beat notes beat_pos = start % 1.0 # Position within beat if 0.4 < beat_pos < 0.6: # Off-beat delay = swing * 0.1 # Max 100ms delay new_note = dict(note) new_note['start'] = start + delay result.append(new_note) else: result.append(note) return result def apply_section_dynamics(self, notes: List[Dict], section: str) -> List[Dict]: """T047-T050: Dinamica por seccion (intro 70%, drop 100%, etc).""" section_scales = { 'intro': 0.70, 'build': 0.85, 'drop': 1.00, 'break': 0.75, 'outro': 0.60, } scale = section_scales.get(section.lower(), 1.0) result = [] for note in notes: vel = note.get('velocity', 100) new_vel = int(vel * scale) new_vel = max(1, min(127, new_vel)) new_note = dict(note) new_note['velocity'] = new_vel result.append(new_note) return result def process_notes(self, notes: List[Dict], section: str = 'drop', humanize: bool = True, groove_style: str = 'shuffle') -> List[Dict]: """Procesamiento completo con todos los efectos.""" result = list(notes) if humanize: result = self.apply_timing_variation(result) result = self.apply_velocity_humanize(result) result = self.apply_note_skip_probability(result) result = self.apply_groove(result, groove_style) result = self.apply_section_dynamics(result, section) return result 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' # ========================================================================= # 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]]: if not notes or keep_probability >= 0.995: return notes 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) return filtered or notes[:1] 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 '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), } }, ], } # 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) 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) -> 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 """ 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) # 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), 'palette': palette or {}, 'tracks': [], } # Generar tracks según género config['tracks'] = self._generate_tracks_for_genre(genre, style, key, scale, structure, sections, profile) 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) } # Crear summary config['summary'] = f""" 🎵 Track Generado: {config['name']} ♩ BPM: {bpm} 🎹 Key: {key} 🎨 Style: {style} 📊 Tracks: {len(config['tracks'])} """ if config.get('reference_track'): config['summary'] += f"🔊 Reference: {config['reference_track'].get('name')}\n" 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) -> List[Dict]: """Genera la configuración de tracks según el género""" track_specs = [] style_text = f"{genre} {style}".lower() 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')) track_specs.insert(15, ('VOCAL CHOP', 'vocal', TRACK_COLORS['vocal'], 'wavetable')) 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', 'vocal', TRACK_COLORS['vocal'], 'wavetable'), ('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: clips = self._build_scene_clips(role, genre, style, key, scale, sections) 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 blueprint_tracks.append(track) 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 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]]) -> List[Dict[str, Any]]: clips = [] for section in sections: notes = self._render_scene_notes(role, genre, style, key, scale, section) if not notes: continue clips.append({ 'scene_index': section['index'], 'length': section['beats'], 'name': f"{role.upper()} - {section['name']}", 'notes': notes, }) return clips def _render_scene_notes(self, role: str, genre: str, style: str, key: str, scale: str, section: Dict[str, Any]) -> List[Dict[str, Any]]: 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']: return self._render_musical_scene(role, genre, key, scale, section) 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() 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 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, 3.0, 0.2, 88)] elif kind == 'break': pattern = [self._make_note(pitch, 1.0, 0.2, 84)] 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 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 [] 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 'latin' in style_text: 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 'latin' in style_text: 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 == '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' if 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': 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]) -> List[Dict[str, Any]]: 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': 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': 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': 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) 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 _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}) 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 = [] # 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 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))}) 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