- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain - Method 1: track.insert_arrangement_clip() [Live 12+] - Method 2: track.create_audio_clip() [Live 11+] - Method 3: arrangement_clips.add_new_clip() [Live 12+] - Method 4: Session->duplicate_clip_to_arrangement [Legacy] - Method 5: Session->Recording [Universal] - Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow - Update skills documentation - Verified: 3 clips created at positions [0, 4, 8] in Arrangement View Closes: Audio injection in Arrangement View
1045 lines
38 KiB
Python
1045 lines
38 KiB
Python
"""
|
|
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)
|