""" Song Generator Engine - Professional Reggaeton Track Generator Este módulo genera configuraciones completas de canciones de reggaeton profesional, incluyendo estructura de secciones, selección de samples basada en perfiles de usuario, y generación de patterns rítmicos y armónicos. Autor: AbletonMCP_AI """ import logging import random from dataclasses import dataclass, field from typing import Dict, List, Optional, Any, Tuple from pathlib import Path import os import datetime logger = logging.getLogger("SongGenerator") # Importar engines existentes try: from .reference_matcher import get_recommended_samples, get_user_profile from .sample_selector import SampleInfo, DrumKit, InstrumentGroup, get_selector _ENGINES_AVAILABLE = True except ImportError: logger.warning("No se pudieron importar engines. Usando modo fallback.") _ENGINES_AVAILABLE = False # ============================================================================= # CONSTANTES Y CONFIGURACIONES # ============================================================================= SUPPORTED_STYLES = ["dembow", "perreo", "romantico", "club", "moombahton"] SUPPORTED_STRUCTURES = ["minimal", "standard", "extended"] SUPPORTED_KEYS = ["Am", "Dm", "Gm", "Cm", "Em", "Bm", "Fm", "F#m", "C#m", "G#m"] # Configuración de estructuras (nombre: [(section_name, bars)]) STRUCTURE_CONFIGS = { "minimal": [ ("intro", 8), ("groove", 16), ("break", 8), ("outro", 8), ], "standard": [ ("intro", 8), ("build", 8), ("drop", 16), ("break", 8), ("drop2", 16), ("outro", 8), ], "extended": [ ("intro", 16), ("build", 8), ("drop", 16), ("break", 8), ("build2", 8), ("drop2", 16), ("peak", 8), ("outro", 16), ], } # Niveles de energía por sección ENERGY_LEVELS = { "intro": 0.3, "groove": 0.6, "build": 0.7, "drop": 0.9, "break": 0.4, "drop2": 0.95, "build2": 0.75, "peak": 1.0, "outro": 0.2, } # Patterns de dembow clásico (16 pasos) DEMBOW_PATTERNS = { "kick": [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0], "snare": [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], "clap": [0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0], "hat_closed": [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1], "hat_open": [0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], "bass": [1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0], } # Variaciones por estilo STYLE_VARIATIONS = { "dembow": { "kick_variation": "standard", "bass_syncopation": 0.3, "hat_density": 1.0, "perc_extra": False, }, "perreo": { "kick_variation": "syncopated", "bass_syncopation": 0.5, "hat_density": 0.8, "perc_extra": True, }, "romantico": { "kick_variation": "sparse", "bass_syncopation": 0.2, "hat_density": 0.6, "perc_extra": False, }, "club": { "kick_variation": "four_on_floor", "bass_syncopation": 0.4, "hat_density": 1.0, "perc_extra": True, }, "moombahton": { "kick_variation": "moombah", "bass_syncopation": 0.4, "hat_density": 0.9, "perc_extra": True, }, } # Roles de instrumentos soportados INSTRUMENT_ROLES = [ "kick", "snare", "clap", "hat_closed", "hat_open", "bass", "synth_lead", "synth_pad", "synth_pluck", "fx" ] # ============================================================================= # CLASES DE DATOS PRINCIPALES # ============================================================================= @dataclass class ClipConfig: """Configuración de un clip (MIDI o Audio).""" name: str start_time: float # En beats duration: float # En beats notes: List[Dict[str, Any]] = field(default_factory=list) sample_path: str = "" is_audio: bool = False def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "start_time": self.start_time, "duration": self.duration, "notes": self.notes, "sample_path": self.sample_path, "is_audio": self.is_audio, } @dataclass class DeviceConfig: """Configuración de un device en la cadena.""" name: str device_type: str # "instrument", "audio_effect", "midi_effect" preset: str = "" parameters: Dict[str, float] = field(default_factory=dict) def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "device_type": self.device_type, "preset": self.preset, "parameters": self.parameters, } @dataclass class TrackConfig: """Configuración completa de una pista.""" name: str track_type: str # "midi" o "audio" instrument_role: str clips: List[ClipConfig] = field(default_factory=list) device_chain: List[DeviceConfig] = field(default_factory=list) volume: float = 0.8 pan: float = 0.0 is_muted: bool = False is_soloed: bool = False # Samples seleccionados para esta pista selected_samples: List[Dict[str, Any]] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "track_type": self.track_type, "instrument_role": self.instrument_role, "clips": [c.to_dict() for c in self.clips], "device_chain": [d.to_dict() for d in self.device_chain], "volume": self.volume, "pan": self.pan, "is_muted": self.is_muted, "is_soloed": self.is_soloed, "selected_samples": self.selected_samples, } @dataclass class Pattern: """Pattern rítmico para un instrumento.""" instrument: str steps: List[int] # 1 = on, 0 = off velocity_variation: float = 0.2 humanize: float = 0.1 def to_dict(self) -> Dict[str, Any]: return { "instrument": self.instrument, "steps": self.steps, "velocity_variation": self.velocity_variation, "humanize": self.humanize, } @dataclass class Section: """Sección de una canción (Intro, Drop, Break, etc.).""" name: str bars: int start_bar: int energy_level: float patterns: Dict[str, Pattern] = field(default_factory=dict) tempo_multiplier: float = 1.0 # Para cambios de tempo # Notas de progresión armónica (si aplica) chord_progression: List[str] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "bars": self.bars, "start_bar": self.start_bar, "energy_level": self.energy_level, "patterns": {k: v.to_dict() for k, v in self.patterns.items()}, "tempo_multiplier": self.tempo_multiplier, "chord_progression": self.chord_progression, } @dataclass class SongConfig: """Configuración completa de una canción generada.""" bpm: float key: str style: str structure: str total_bars: int sections: List[Section] = field(default_factory=list) tracks: List[TrackConfig] = field(default_factory=list) # Metadatos generated_from_reference: str = "" generation_timestamp: str = "" variation_seed: int = 0 # Samples usados drum_kit: Dict[str, Any] = field(default_factory=dict) bass_samples: List[Dict[str, Any]] = field(default_factory=list) synth_samples: List[Dict[str, Any]] = field(default_factory=list) fx_samples: List[Dict[str, Any]] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: return { "bpm": self.bpm, "key": self.key, "style": self.style, "structure": self.structure, "total_bars": self.total_bars, "sections": [s.to_dict() for s in self.sections], "tracks": [t.to_dict() for t in self.tracks], "generated_from_reference": self.generated_from_reference, "generation_timestamp": self.generation_timestamp, "variation_seed": self.variation_seed, "drum_kit": self.drum_kit, "bass_samples": self.bass_samples, "synth_samples": self.synth_samples, "fx_samples": self.fx_samples, } # ============================================================================= # CLASE PRINCIPAL: REGGAETON GENERATOR # ============================================================================= class ReggaetonGenerator: """ Generador profesional de tracks de reggaeton. Genera configuraciones completas de canciones incluyendo: - Estructura de secciones (Intro, Drop, Break, etc.) - Selección inteligente de samples basada en perfiles de usuario - Patterns rítmicos adaptados al estilo - Configuración de pistas y dispositivos """ def __init__(self): self._user_profile: Optional[Dict[str, Any]] = None self._selected_samples: Dict[str, List[Dict[str, Any]]] = {} self._variation_seed: int = random.randint(1, 10000) random.seed(self._variation_seed) def generate(self, bpm: float = 95.0, key: str = "Am", style: str = "dembow", structure: str = "standard") -> SongConfig: """ Genera una configuración completa de canción. Args: bpm: Tempo en beats por minuto (80-110 recomendado) key: Tonalidad (Am, Dm, Gm, etc.) style: Estilo (dembow, perreo, romantico, club, moombahton) structure: Estructura (minimal, standard, extended) Returns: SongConfig con toda la información de la canción generada """ logger.info("Generando canción: BPM=%.1f, Key=%s, Style=%s, Structure=%s", bpm, key, style, structure) # Validar parámetros bpm = self._validate_bpm(bpm) key = self._validate_key(key) style = self._validate_style(style) structure = self._validate_structure(structure) # Seleccionar samples self._select_samples_for_song(style, key, bpm) # Crear estructura de secciones sections = self._create_sections(structure) # Calcular total de compases total_bars = sum(s.bars for s in sections) # Crear configuración de pistas tracks = self._create_tracks(style, sections, bpm, key) # Construir SongConfig config = SongConfig( bpm=bpm, key=key, style=style, structure=structure, total_bars=total_bars, sections=sections, tracks=tracks, variation_seed=self._variation_seed, generation_timestamp=datetime.datetime.now().isoformat(), drum_kit=self._get_drum_kit_info(), bass_samples=self._selected_samples.get("bass", []), synth_samples=self._selected_samples.get("synth", []), fx_samples=self._selected_samples.get("fx", []), ) logger.info("Canción generada: %d compases, %d pistas", total_bars, len(tracks)) return config def generate_from_reference(self, reference_path: str, bpm: float = 0, key: str = "") -> SongConfig: """ Genera una canción basada en un archivo de referencia. Analiza el archivo de referencia, obtiene el perfil de usuario y genera una canción que suena similar. Args: reference_path: Ruta al archivo de audio de referencia bpm: Tempo deseado (0 = usar el detectado en referencia) key: Tonalidad deseada ("" = usar la detectada en referencia) Returns: SongConfig basado en la referencia """ logger.info("Generando desde referencia: %s", reference_path) try: # Obtener perfil de usuario desde referencia profile = get_user_profile(reference_path=reference_path) self._user_profile = profile # Determinar BPM y Key if bpm <= 0: bpm = profile.get("preferred_bpm", 95.0) if not key: key = profile.get("preferred_key", "Am") # Detectar estilo preferido basado en características style = self._detect_style_from_profile(profile) # Generar con la configuración detectada config = self.generate( bpm=bpm, key=key, style=style, structure="standard" ) config.generated_from_reference = reference_path logger.info("Canción generada desde referencia: BPM=%.1f, Key=%s", bpm, key) return config except Exception as e: logger.error("Error generando desde referencia: %s. Fallback a defaults.", e) return self.generate(bpm=bpm or 95.0, key=key or "Am") # ------------------------------------------------------------------------- # MÉTODOS DE VALIDACIÓN # ------------------------------------------------------------------------- def _validate_bpm(self, bpm: float) -> float: """Valida y normaliza el BPM.""" if bpm < 60 or bpm > 150: logger.warning("BPM fuera de rango reggaeton (%.1f), usando 95", bpm) return 95.0 return bpm def _validate_key(self, key: str) -> str: """Valida y normaliza la tonalidad.""" key = key.strip().capitalize() if key not in SUPPORTED_KEYS: logger.warning("Key no soportada (%s), usando Am", key) return "Am" return key def _validate_style(self, style: str) -> str: """Valida y normaliza el estilo.""" style = style.lower().strip() if style not in SUPPORTED_STYLES: logger.warning("Style no soportado (%s), usando dembow", style) return "dembow" return style def _validate_structure(self, structure: str) -> str: """Valida y normaliza la estructura.""" structure = structure.lower().strip() if structure not in SUPPORTED_STRUCTURES: logger.warning("Structure no soportada (%s), usando standard", structure) return "standard" return structure # ------------------------------------------------------------------------- # SELECCIÓN DE SAMPLES # ------------------------------------------------------------------------- def _select_samples_for_song(self, style: str, key: str, bpm: float): """Selecciona todos los samples necesarios para la canción.""" logger.info("Seleccionando samples para %s en %s @ %.1f BPM", style, key, bpm) self._selected_samples = {} if not _ENGINES_AVAILABLE: logger.warning("Engines no disponibles, usando samples por defecto") return try: # Seleccionar samples por rol usando el motor de recomendaciones roles_to_select = { "kick": 3, "snare": 3, "clap": 2, "hat_closed": 3, "hat_open": 2, "bass": 5, "synth": 5, "fx": 3, } for role, count in roles_to_select.items(): samples = get_recommended_samples(role=role, count=count) self._selected_samples[role] = samples logger.debug("Seleccionados %d samples para %s", len(samples), role) except Exception as e: logger.error("Error seleccionando samples: %s", e) def _get_drum_kit_info(self) -> Dict[str, Any]: """Retorna información del drum kit seleccionado.""" kit = { "kick": self._selected_samples.get("kick", [{}])[0] if self._selected_samples.get("kick") else {}, "snare": self._selected_samples.get("snare", [{}])[0] if self._selected_samples.get("snare") else {}, "clap": self._selected_samples.get("clap", [{}])[0] if self._selected_samples.get("clap") else {}, "hat_closed": self._selected_samples.get("hat_closed", [{}])[0] if self._selected_samples.get("hat_closed") else {}, "hat_open": self._selected_samples.get("hat_open", [{}])[0] if self._selected_samples.get("hat_open") else {}, } return kit # ------------------------------------------------------------------------- # CREACIÓN DE ESTRUCTURA # ------------------------------------------------------------------------- def _create_sections(self, structure: str) -> List[Section]: """Crea la estructura de secciones de la canción.""" sections_config = STRUCTURE_CONFIGS[structure] sections = [] current_bar = 0 for section_name, bars in sections_config: energy = ENERGY_LEVELS.get(section_name, 0.5) # Crear patterns para esta sección patterns = self._create_patterns_for_section(section_name, energy) section = Section( name=section_name, bars=bars, start_bar=current_bar, energy_level=energy, patterns=patterns, ) sections.append(section) current_bar += bars return sections def _create_patterns_for_section(self, section_name: str, energy: float) -> Dict[str, Pattern]: """Crea los patterns rítmicos para una sección.""" patterns = {} # Adaptar patterns según la energía de la sección if section_name in ["intro", "outro"]: # Intro y outro: patterns mínimos patterns["kick"] = self._adapt_pattern(DEMBOW_PATTERNS["kick"], density=0.5) patterns["snare"] = self._adapt_pattern(DEMBOW_PATTERNS["snare"], density=0.3) patterns["hat_closed"] = self._adapt_pattern(DEMBOW_PATTERNS["hat_closed"], density=0.6) elif section_name in ["build", "build2"]: # Build: aumentar intensidad patterns["kick"] = self._adapt_pattern(DEMBOW_PATTERNS["kick"], density=0.8) patterns["snare"] = self._adapt_pattern(DEMBOW_PATTERNS["snare"], density=0.6) patterns["hat_closed"] = self._adapt_pattern(DEMBOW_PATTERNS["hat_closed"], density=0.9) patterns["bass"] = self._adapt_pattern(DEMBOW_PATTERNS["bass"], density=0.7) elif section_name in ["drop", "drop2"]: # Drop: full dembow patterns["kick"] = Pattern("kick", DEMBOW_PATTERNS["kick"]) patterns["snare"] = Pattern("snare", DEMBOW_PATTERNS["snare"]) patterns["hat_closed"] = Pattern("hat_closed", DEMBOW_PATTERNS["hat_closed"]) patterns["hat_open"] = Pattern("hat_open", DEMBOW_PATTERNS["hat_open"]) patterns["bass"] = Pattern("bass", DEMBOW_PATTERNS["bass"]) elif section_name == "break": # Break: drums mínimos, espacio para vocals patterns["kick"] = self._adapt_pattern(DEMBOW_PATTERNS["kick"], density=0.3) patterns["snare"] = Pattern("snare", [0] * 16) patterns["hat_closed"] = self._adapt_pattern(DEMBOW_PATTERNS["hat_closed"], density=0.4) elif section_name == "groove": # Groove: dembow estándar patterns["kick"] = Pattern("kick", DEMBOW_PATTERNS["kick"]) patterns["snare"] = Pattern("snare", DEMBOW_PATTERNS["snare"]) patterns["hat_closed"] = Pattern("hat_closed", DEMBOW_PATTERNS["hat_closed"]) patterns["bass"] = Pattern("bass", DEMBOW_PATTERNS["bass"]) elif section_name == "peak": # Peak: máxima intensidad patterns["kick"] = self._adapt_pattern(DEMBOW_PATTERNS["kick"], density=1.0) patterns["snare"] = self._adapt_pattern(DEMBOW_PATTERNS["snare"], density=1.0) patterns["clap"] = Pattern("clap", DEMBOW_PATTERNS["snare"]) patterns["hat_closed"] = self._adapt_pattern(DEMBOW_PATTERNS["hat_closed"], density=1.0) patterns["hat_open"] = Pattern("hat_open", DEMBOW_PATTERNS["hat_open"]) patterns["bass"] = self._adapt_pattern(DEMBOW_PATTERNS["bass"], density=1.0) return patterns def _adapt_pattern(self, base_pattern: List[int], density: float) -> Pattern: """Adapta un pattern base a una densidad específica.""" if density >= 1.0: return Pattern("unknown", base_pattern[:]) adapted = [] for step in base_pattern: if step == 1 and random.random() > density: adapted.append(0) else: adapted.append(step) return Pattern("unknown", adapted) # ------------------------------------------------------------------------- # CREACIÓN DE PISTAS # ------------------------------------------------------------------------- def _create_tracks(self, style: str, sections: List[Section], bpm: float, key: str) -> List[TrackConfig]: """Crea la configuración de todas las pistas.""" tracks = [] # Pista 1: Kick kick_track = self._create_drum_track("Kick", "kick", sections, bpm) tracks.append(kick_track) # Pista 2: Snare snare_track = self._create_drum_track("Snare", "snare", sections, bpm) tracks.append(snare_track) # Pista 3: Clap (si aplica según estilo) if style in ["club", "perreo", "moombahton"]: clap_track = self._create_drum_track("Clap", "clap", sections, bpm) tracks.append(clap_track) # Pista 4: Hi-Hats hat_track = self._create_drum_track("Hi-Hats", "hat_closed", sections, bpm) tracks.append(hat_track) # Pista 5: Open Hat open_hat_track = self._create_drum_track("Open Hat", "hat_open", sections, bpm) tracks.append(open_hat_track) # Pista 6: Bass bass_track = self._create_bass_track(sections, bpm, key) tracks.append(bass_track) # Pista 7: Synth Lead synth_track = self._create_synth_track("Lead", sections, bpm, key) tracks.append(synth_track) # Pista 8: FX fx_track = self._create_fx_track(sections, bpm) tracks.append(fx_track) # Aplicar variaciones de estilo self._apply_style_variations(tracks, style) return tracks def _create_drum_track(self, name: str, role: str, sections: List[Section], bpm: float) -> TrackConfig: """Crea una pista de percusión.""" clips = [] current_time = 0.0 for section in sections: # Crear clips para esta sección basado en el pattern if role in section.patterns: pattern = section.patterns[role] notes = self._pattern_to_notes(pattern, current_time, section.bars, bpm) clip = ClipConfig( name=f"{name} - {section.name}", start_time=current_time, duration=section.bars * 4.0, # 4 beats por compás notes=notes, ) clips.append(clip) current_time += section.bars * 4.0 # Samples seleccionados samples = self._selected_samples.get(role, []) return TrackConfig( name=name, track_type="midi", instrument_role=role, clips=clips, selected_samples=samples, device_chain=[ DeviceConfig("Drum Rack", "instrument", "default"), ], ) def _pattern_to_notes(self, pattern: Pattern, start_time: float, bars: int, bpm: float) -> List[Dict[str, Any]]: """Convierte un pattern a notas MIDI.""" notes = [] beats_per_step = 4.0 / 16 # 16 steps en 4 beats (un compás) for bar in range(bars): for step_idx, step in enumerate(pattern.steps): if step == 1: note_time = start_time + (bar * 4.0) + (step_idx * beats_per_step) velocity = 100 + random.randint(-20, 20) # Variación de velocity notes.append({ "pitch": 36 if pattern.instrument == "kick" else 38 if pattern.instrument == "snare" else 39 if pattern.instrument == "clap" else 42 if pattern.instrument == "hat_closed" else 46 if pattern.instrument == "hat_open" else 36, "start_time": note_time, "duration": 0.25, "velocity": max(1, min(127, velocity)), }) return notes def _create_bass_track(self, sections: List[Section], bpm: float, key: str) -> TrackConfig: """Crea la pista de bajo.""" clips = [] current_time = 0.0 # Notas raíz según la tonalidad root_notes = { "Am": 57, "Dm": 62, "Gm": 55, "Cm": 60, "Em": 64, "Bm": 71, "Fm": 65, "F#m": 66, "C#m": 61, "G#m": 68, } root_note = root_notes.get(key, 57) for section in sections: if "bass" in section.patterns: pattern = section.patterns["bass"] notes = [] beats_per_step = 4.0 / 16 for bar in range(section.bars): for step_idx, step in enumerate(pattern.steps): if step == 1: note_time = current_time + (bar * 4.0) + (step_idx * beats_per_step) # Variar pitch según progresión pitch = root_note if section.energy_level > 0.7 and random.random() > 0.7: pitch += 7 # Quinta notes.append({ "pitch": pitch, "start_time": note_time, "duration": 0.5, "velocity": 110, }) clip = ClipConfig( name=f"Bass - {section.name}", start_time=current_time, duration=section.bars * 4.0, notes=notes, ) clips.append(clip) current_time += section.bars * 4.0 return TrackConfig( name="Bass", track_type="midi", instrument_role="bass", clips=clips, selected_samples=self._selected_samples.get("bass", []), device_chain=[ DeviceConfig("Operator", "instrument", "bass_preset"), DeviceConfig("EQ Eight", "audio_effect", "bass_eq"), ], ) def _create_synth_track(self, synth_type: str, sections: List[Section], bpm: float, key: str) -> TrackConfig: """Crea una pista de sintetizador.""" clips = [] current_time = 0.0 # Notas de la escala menor scale_notes = self._get_scale_notes(key) for section in sections: # Solo tocar en secciones con suficiente energía if section.energy_level >= 0.6: notes = [] # Crear progresión armónica simple chord_progression = [0, 3, 0, 5] # i - iv - i - VI for bar in range(section.bars): chord_idx = bar % len(chord_progression) root_offset = chord_progression[chord_idx] # Tocar notas del acorde for beat in range(4): if random.random() > 0.3: # No tocar en todos los beats note_time = current_time + (bar * 4.0) + beat pitch = scale_notes[(root_offset + random.choice([0, 2, 4])) % 7] notes.append({ "pitch": pitch, "start_time": note_time, "duration": 1.0, "velocity": int(80 + section.energy_level * 40), }) clip = ClipConfig( name=f"Synth {synth_type} - {section.name}", start_time=current_time, duration=section.bars * 4.0, notes=notes, ) clips.append(clip) current_time += section.bars * 4.0 return TrackConfig( name=f"Synth {synth_type}", track_type="midi", instrument_role="synth_lead", clips=clips, selected_samples=self._selected_samples.get("synth", []), device_chain=[ DeviceConfig("Wavetable", "instrument", "lead_preset"), DeviceConfig("Reverb", "audio_effect", "synth_reverb"), DeviceConfig("Delay", "audio_effect", "synth_delay"), ], ) def _create_fx_track(self, sections: List[Section], bpm: float) -> TrackConfig: """Crea la pista de efectos.""" clips = [] current_time = 0.0 for section in sections: # FX en transiciones importantes if section.name in ["build", "build2"]: # Riser antes del drop notes = [] for i in range(int(section.bars * 4)): notes.append({ "pitch": 60 + i, "start_time": current_time + i, "duration": 0.5, "velocity": 80 + i * 2, }) clip = ClipConfig( name=f"FX Riser - {section.name}", start_time=current_time, duration=section.bars * 4.0, notes=notes, ) clips.append(clip) elif section.name in ["drop", "drop2", "peak"]: # Impact/Hit al inicio notes = [{ "pitch": 36, "start_time": current_time, "duration": 2.0, "velocity": 120, }] clip = ClipConfig( name=f"FX Impact - {section.name}", start_time=current_time, duration=section.bars * 4.0, notes=notes, ) clips.append(clip) current_time += section.bars * 4.0 return TrackConfig( name="FX", track_type="midi", instrument_role="fx", clips=clips, selected_samples=self._selected_samples.get("fx", []), device_chain=[ DeviceConfig("Simpler", "instrument", "fx_sampler"), ], ) def _get_scale_notes(self, key: str) -> List[int]: """Retorna las notas MIDI de la escala menor dada la tonalidad.""" root_notes = { "Am": 57, "Dm": 62, "Gm": 55, "Cm": 60, "Em": 64, "Bm": 71, "Fm": 65, "F#m": 66, "C#m": 61, "G#m": 68, } root = root_notes.get(key, 57) # Escala menor natural: 0, 2, 3, 5, 7, 8, 10 intervals = [0, 2, 3, 5, 7, 8, 10] return [root + interval for interval in intervals] def _apply_style_variations(self, tracks: List[TrackConfig], style: str): """Aplica variaciones específicas del estilo a las pistas.""" variations = STYLE_VARIATIONS.get(style, STYLE_VARIATIONS["dembow"]) # Ajustar volumes según estilo for track in tracks: if track.instrument_role == "kick": track.volume = 0.9 if variations["kick_variation"] != "sparse" else 0.7 elif track.instrument_role == "bass": track.volume = 0.85 if variations["bass_syncopation"] > 0.3 else 0.75 elif track.instrument_role == "hat_closed": track.volume = 0.7 * variations["hat_density"] def _detect_style_from_profile(self, profile: Dict[str, Any]) -> str: """Detecta el estilo preferido basado en el perfil de usuario.""" bpm = profile.get("preferred_bpm", 95.0) roles = profile.get("preferred_roles", []) # Heurísticas simples basadas en BPM if bpm > 105: return "club" elif bpm < 88: return "romantico" elif bpm > 98: return "perreo" # Default return "dembow" # ============================================================================= # SONG GENERATOR (Alias para compatibilidad) # ============================================================================= class SongGenerator(ReggaetonGenerator): """ Alias de ReggaetonGenerator para compatibilidad con imports existentes. """ def generate_config(self, genre: str = "reggaeton", style: str = "", bpm: float = 0, key: str = "Am", structure: str = "standard") -> Dict[str, Any]: """ Método de compatibilidad que emula la interfaz antigua. Convierte los parámetros y llama al nuevo método generate(). """ # Usar style como style si está presente, si no usar genre actual_style = style if style else genre # Determinar BPM actual_bpm = bpm if bpm > 0 else 95.0 config = self.generate( bpm=actual_bpm, key=key, style=actual_style, structure=structure ) return config.to_dict() # ============================================================================= # FUNCIONES DE CONVENIENCIA # ============================================================================= _generator: Optional[ReggaetonGenerator] = None def get_song_generator() -> ReggaetonGenerator: """Retorna instancia global del generador.""" global _generator if _generator is None: _generator = ReggaetonGenerator() return _generator def generate_song(bpm: float = 95.0, key: str = "Am", style: str = "dembow", structure: str = "standard") -> Dict[str, Any]: """ Función de conveniencia para generar una canción. Returns: Diccionario con la configuración de la canción. """ generator = get_song_generator() config = generator.generate(bpm, key, style, structure) return config.to_dict() def generate_from_reference(reference_path: str, bpm: float = 0, key: str = "") -> Dict[str, Any]: """ Función de conveniencia para generar desde una referencia. Returns: Diccionario con la configuración basada en la referencia. """ generator = get_song_generator() config = generator.generate_from_reference(reference_path, bpm, key) return config.to_dict() def get_supported_styles() -> List[str]: """Retorna la lista de estilos soportados.""" return SUPPORTED_STYLES.copy() def get_supported_structures() -> List[str]: """Retorna la lista de estructuras soportadas.""" return SUPPORTED_STRUCTURES.copy() # ============================================================================= # MAIN / TEST # ============================================================================= if __name__ == "__main__": logging.basicConfig(level=logging.INFO) print("=" * 70) print("SONG GENERATOR - Reggaeton Professional Track Generator") print("=" * 70) # Test 1: Generar canción standard print("\n1. Generando canción 'standard' en estilo 'dembow'...") generator = ReggaetonGenerator() config = generator.generate(bpm=95, key="Am", style="dembow", structure="standard") print(f" BPM: {config.bpm}") print(f" Key: {config.key}") print(f" Style: {config.style}") print(f" Structure: {config.structure}") print(f" Total Bars: {config.total_bars}") print(f" Sections: {[s.name for s in config.sections]}") print(f" Tracks: {[t.name for t in config.tracks]}") # Test 2: Generar canción minimal print("\n2. Generando canción 'minimal' en estilo 'perreo'...") config2 = generator.generate(bpm=98, key="Gm", style="perreo", structure="minimal") print(f" Total Bars: {config2.total_bars}") print(f" Sections: {[s.name for s in config2.sections]}") # Test 3: Generar canción extended print("\n3. Generando canción 'extended' en estilo 'club'...") config3 = generator.generate(bpm=105, key="Dm", style="club", structure="extended") print(f" Total Bars: {config3.total_bars}") print(f" Sections: {[s.name for s in config3.sections]}") # Test 4: Mostrar samples seleccionados print("\n4. Samples seleccionados:") for role, samples in generator._selected_samples.items(): if samples: print(f" {role}: {len(samples)} samples") for s in samples[:2]: print(f" - {s.get('name', 'unknown')}") print("\n" + "=" * 70) print("Test completado!") print("=" * 70)