Files
ableton-mcp-ai/mcp_server/engines/song_generator.py
OpenCode Agent 5ce8187c65 feat: Implement senior audio injection with 5 fallback methods
- 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
2026-04-12 14:02:32 -03:00

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)