""" Arrangement Engine - Arrangement View and Automation Engine Este módulo proporciona herramientas avanzadas para trabajar con Arrangement View en Ableton Live, incluyendo construcción de estructuras, automatización de parámetros, creación de efectos FX y procesamiento de samples. Autor: AbletonMCP_AI """ import logging import random from dataclasses import dataclass, field from typing import Dict, List, Optional, Any, Tuple, Union from pathlib import Path import os import math logger = logging.getLogger("ArrangementEngine") # ============================================================================= # CONSTANTES Y CONFIGURACIONES # ============================================================================= # Estructuras de arrangement predefinidas ARRANGEMENT_STRUCTURES = { "intro_build_drop_break_outro": [ ("intro", 8), ("build", 8), ("drop", 16), ("break", 8), ("drop2", 16), ("outro", 8), ], "intro_drop_break_outro": [ ("intro", 8), ("drop", 16), ("break", 8), ("outro", 8), ], "extended": [ ("intro", 16), ("build", 8), ("drop", 16), ("break1", 8), ("build2", 8), ("drop2", 16), ("break2", 8), ("peak", 8), ("outro", 16), ], } # Configuraciones de automatización por defecto DEFAULT_FILTER_FREQ_START = 200.0 DEFAULT_FILTER_FREQ_END = 20000.0 DEFAULT_REVERB_WET_START = 0.0 DEFAULT_REVERB_WET_END = 0.5 DEFAULT_VOLUME_START = 0.0 DEFAULT_VOLUME_END = 0.85 DEFAULT_DELAY_FEEDBACK_START = 0.1 DEFAULT_DELAY_FEEDBACK_END = 0.6 # Tipos de secciones y sus niveles de energía SECTION_ENERGY_LEVELS = { "intro": 0.2, "build": 0.7, "drop": 1.0, "break": 0.3, "break1": 0.3, "break2": 0.4, "drop2": 1.0, "outro": 0.15, "build2": 0.75, "peak": 1.0, } # ============================================================================= # CLASES DE DATOS # ============================================================================= @dataclass class SectionMarker: """Representa un marcador de sección en el arrangement.""" name: str start_bar: int end_bar: int color: int = 0 def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "start_bar": self.start_bar, "end_bar": self.end_bar, "color": self.color, } @dataclass class AutomationPoint: """Punto de automatización (tiempo, valor).""" time: float # En beats value: float def to_dict(self) -> Dict[str, Any]: return { "time": self.time, "value": self.value, } @dataclass class AutomationEnvelope: """Envelope de automatización completo.""" parameter_name: str device_name: str points: List[AutomationPoint] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: return { "parameter_name": self.parameter_name, "device_name": self.device_name, "points": [p.to_dict() for p in self.points], } @dataclass class ArrangementClip: """Representa un clip en el Arrangement View.""" name: str track_index: int start_time: float # En beats duration: float is_audio: bool = False sample_path: str = "" notes: List[Dict[str, Any]] = field(default_factory=list) def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "track_index": self.track_index, "start_time": self.start_time, "duration": self.duration, "is_audio": self.is_audio, "sample_path": self.sample_path, "notes": self.notes, } @dataclass class ArrangementSection: """Sección completa del arrangement con clips y automatizaciones.""" name: str start_bar: int bars: int clips: List[ArrangementClip] = field(default_factory=list) automations: List[AutomationEnvelope] = field(default_factory=list) energy_level: float = 0.5 def to_dict(self) -> Dict[str, Any]: return { "name": self.name, "start_bar": self.start_bar, "bars": self.bars, "clips": [c.to_dict() for c in self.clips], "automations": [a.to_dict() for a in self.automations], "energy_level": self.energy_level, } @dataclass class ArrangementConfig: """Configuración completa del arrangement.""" total_bars: int sections: List[ArrangementSection] = field(default_factory=list) markers: List[SectionMarker] = field(default_factory=list) tempo: float = 95.0 def to_dict(self) -> Dict[str, Any]: return { "total_bars": self.total_bars, "sections": [s.to_dict() for s in self.sections], "markers": [m.to_dict() for m in self.markers], "tempo": self.tempo, } # ============================================================================= # CLASE 1: ARRANGEMENT BUILDER (T021-T025) # ============================================================================= class ArrangementBuilder: """ Constructor de estructuras de Arrangement View. Crea estructuras de canción completas (Intro→Build→Drop→Break→Outro) y gestiona la transición entre Session View y Arrangement View. """ def __init__(self): self._config: Optional[ArrangementConfig] = None self._sections: List[ArrangementSection] = [] self._markers: List[SectionMarker] = [] def build_arrangement_structure(self, song_config: Dict[str, Any]) -> ArrangementConfig: """ T021: Crea estructura completa Intro→Build→Drop→Break→Outro. Args: song_config: Configuración de canción con BPM, estructura, etc. Returns: ArrangementConfig con toda la estructura """ structure_name = song_config.get("structure", "standard") bpm = song_config.get("bpm", 95.0) # Obtener configuración de estructura if structure_name in ARRANGEMENT_STRUCTURES: structure = ARRANGEMENT_STRUCTURES[structure_name] else: structure = ARRANGEMENT_STRUCTURES["intro_build_drop_break_outro"] total_bars = sum(bars for _, bars in structure) # Crear secciones current_bar = 0 sections = [] markers = [] for section_name, bars in structure: energy = SECTION_ENERGY_LEVELS.get(section_name, 0.5) section = ArrangementSection( name=section_name, start_bar=current_bar, bars=bars, energy_level=energy, ) sections.append(section) # Crear marcador marker = SectionMarker( name=section_name.upper(), start_bar=current_bar, end_bar=current_bar + bars, color=self._get_section_color(section_name), ) markers.append(marker) current_bar += bars config = ArrangementConfig( total_bars=total_bars, sections=sections, markers=markers, tempo=bpm, ) self._config = config self._sections = sections self._markers = markers logger.info("Estructura de arrangement creada: %d compases, %d secciones", total_bars, len(sections)) return config def create_section_marker(self, name: str, start_bar: int) -> SectionMarker: """ T022: Crea un marcador de sección. Args: name: Nombre del marcador start_bar: Compás inicial Returns: SectionMarker creado """ # Detectar duración basada en nombre de sección default_bars = { "intro": 8, "build": 8, "drop": 16, "break": 8, "outro": 8, "peak": 8, } bars = default_bars.get(name.lower(), 8) marker = SectionMarker( name=name.upper(), start_bar=start_bar, end_bar=start_bar + bars, color=self._get_section_color(name), ) self._markers.append(marker) logger.info("Marcador creado: %s en compás %d", name, start_bar) return marker def duplicate_clips_to_arrangement( self, session_clips: List[Dict[str, Any]], arrangement_positions: List[Dict[str, Any]] ) -> List[ArrangementClip]: """ T023: Copia clips de Session View a Arrangement View. Args: session_clips: Lista de clips de Session View arrangement_positions: Posiciones donde colocar cada clip Returns: Lista de ArrangementClip creados """ arrangement_clips = [] for i, clip_info in enumerate(session_clips): if i >= len(arrangement_positions): break pos = arrangement_positions[i] arrangement_clip = ArrangementClip( name=clip_info.get("name", f"Clip {i}"), track_index=pos.get("track_index", clip_info.get("track_index", 0)), start_time=pos.get("start_time", pos.get("start_bar", 0) * 4.0), duration=clip_info.get("duration", 4.0), is_audio=clip_info.get("is_audio", False), sample_path=clip_info.get("sample_path", ""), notes=clip_info.get("notes", []), ) arrangement_clips.append(arrangement_clip) # Añadir a la sección correspondiente start_bar = int(arrangement_clip.start_time / 4.0) for section in self._sections: if section.start_bar <= start_bar < section.start_bar + section.bars: section.clips.append(arrangement_clip) break logger.info("%d clips duplicados a Arrangement View", len(arrangement_clips)) return arrangement_clips def create_arrangement_midi_clip( self, track_index: int, start_time: float, length: float, notes: List[Dict[str, Any]] ) -> ArrangementClip: """ T024: Crea un clip MIDI en Arrangement View. Args: track_index: Índice de la pista start_time: Tiempo de inicio en beats length: Duración en beats notes: Lista de notas MIDI Returns: ArrangementClip creado """ clip = ArrangementClip( name=f"MIDI Clip - Track {track_index}", track_index=track_index, start_time=start_time, duration=length, is_audio=False, notes=notes, ) # Añadir a sección correspondiente start_bar = int(start_time / 4.0) for section in self._sections: if section.start_bar <= start_bar < section.start_bar + section.bars: section.clips.append(clip) break logger.info("Clip MIDI creado: track %d, %d notas", track_index, len(notes)) return clip def create_arrangement_audio_clip( self, track_index: int, sample_path: str, start_time: float, length: float ) -> ArrangementClip: """ T025: Crea un clip de audio en Arrangement View. Args: track_index: Índice de la pista sample_path: Ruta al archivo de audio start_time: Tiempo de inicio en beats length: Duración en beats Returns: ArrangementClip creado """ clip = ArrangementClip( name=os.path.basename(sample_path) if sample_path else "Audio Clip", track_index=track_index, start_time=start_time, duration=length, is_audio=True, sample_path=sample_path, ) # Añadir a sección correspondiente start_bar = int(start_time / 4.0) for section in self._sections: if section.start_bar <= start_bar < section.start_bar + section.bars: section.clips.append(clip) break logger.info("Clip de audio creado: track %d, %s", track_index, os.path.basename(sample_path)) return clip def fill_arrangement_with_song(self, song_config: Dict[str, Any]) -> ArrangementConfig: """ Pipeline completo: crea estructura y llena con clips desde Session View. Args: song_config: Configuración completa de la canción Returns: ArrangementConfig final """ # 1. Crear estructura base config = self.build_arrangement_structure(song_config) # 2. Procesar tracks de la configuración tracks = song_config.get("tracks", []) for track_idx, track in enumerate(tracks): clips = track.get("clips", []) for clip in clips: start_time = clip.get("start_time", 0.0) duration = clip.get("duration", 4.0) notes = clip.get("notes", []) sample_path = clip.get("sample_path", "") if sample_path: # Es un clip de audio self.create_arrangement_audio_clip( track_index=track_idx, sample_path=sample_path, start_time=start_time, length=duration ) elif notes: # Es un clip MIDI self.create_arrangement_midi_clip( track_index=track_idx, start_time=start_time, length=duration, notes=notes ) logger.info("Pipeline completado: arrangement lleno con %d tracks", len(tracks)) return config def _get_section_color(self, section_name: str) -> int: """Retorna color para una sección según su tipo.""" colors = { "intro": 1, # Azul "build": 3, # Naranja "drop": 5, # Rojo "break": 2, # Verde "break1": 2, "break2": 2, "drop2": 5, "outro": 6, # Púrpura "peak": 4, # Amarillo } return colors.get(section_name.lower(), 0) # ============================================================================= # CLASE 2: AUTOMATION ENGINE (T026-T030) # ============================================================================= class AutomationEngine: """ Motor de automatización para parámetros de devices y mezcla. Crea envelopes de automatización para efectos comunes como filtros, reverb, volumen, delay y envíos. """ def __init__(self): self._envelopes: List[AutomationEnvelope] = [] def automate_filter( self, track_index: int, start_bar: int, end_bar: int, start_freq: float = DEFAULT_FILTER_FREQ_START, end_freq: float = DEFAULT_FILTER_FREQ_END, curve: str = "linear" ) -> AutomationEnvelope: """ T026: Automatización de cutoff de AutoFilter (sweep). Args: track_index: Índice de la pista start_bar: Compás inicial end_bar: Compás final start_freq: Frecuencia inicial en Hz end_freq: Frecuencia final en Hz curve: Tipo de curva ("linear", "exponential", "logarithmic") Returns: AutomationEnvelope creado """ start_time = start_bar * 4.0 end_time = end_bar * 4.0 duration = end_time - start_time points = [] num_points = max(8, int(duration / 4)) # Un punto por compás mínimo for i in range(num_points + 1): t = i / num_points time = start_time + t * duration if curve == "exponential": t = t * t elif curve == "logarithmic": t = math.sqrt(t) # Interpolación logarítmica para frecuencia freq = start_freq * ((end_freq / start_freq) ** t) points.append(AutomationPoint(time=time, value=freq)) envelope = AutomationEnvelope( parameter_name="Frequency", device_name="AutoFilter", points=points, ) self._envelopes.append(envelope) logger.info("AutoFilter sweep: %d->%d compases, %.0f->%.0f Hz", start_bar, end_bar, start_freq, end_freq) return envelope def automate_reverb( self, track_index: int, start_bar: int, end_bar: int, dry_wet_start: float = DEFAULT_REVERB_WET_START, dry_wet_end: float = DEFAULT_REVERB_WET_END, parameter: str = "Dry/Wet" ) -> AutomationEnvelope: """ T027: Automatización de wet/dry de reverb. Args: track_index: Índice de la pista start_bar: Compás inicial end_bar: Compás final dry_wet_start: Valor inicial (0.0-1.0) dry_wet_end: Valor final (0.0-1.0) parameter: Nombre del parámetro a automatizar Returns: AutomationEnvelope creado """ start_time = start_bar * 4.0 end_time = end_bar * 4.0 duration = end_time - start_time points = [] num_points = max(4, int(duration / 4)) for i in range(num_points + 1): t = i / num_points time = start_time + t * duration # Interpolación lineal value = dry_wet_start + (dry_wet_end - dry_wet_start) * t points.append(AutomationPoint(time=time, value=value)) envelope = AutomationEnvelope( parameter_name=parameter, device_name="Reverb", points=points, ) self._envelopes.append(envelope) logger.info("Reverb automation: %d->%d compases, %.2f->%.2f", start_bar, end_bar, dry_wet_start, dry_wet_end) return envelope def automate_volume( self, track_index: int, start_bar: int, end_bar: int, start_vol: float = DEFAULT_VOLUME_START, end_vol: float = DEFAULT_VOLUME_END, fade_type: str = "in" ) -> AutomationEnvelope: """ T028: Automatización de volumen (fade in/out). Args: track_index: Índice de la pista start_bar: Compás inicial end_bar: Compás final start_vol: Volumen inicial (0.0-1.0) end_vol: Volumen final (0.0-1.0) fade_type: "in", "out", o "crossfade" Returns: AutomationEnvelope creado """ start_time = start_bar * 4.0 end_time = end_bar * 4.0 duration = end_time - start_time points = [] num_points = max(4, int(duration / 4)) for i in range(num_points + 1): t = i / num_points time = start_time + t * duration # Curva de fade más natural if fade_type == "in": t = t * t # Curva exponencial suave elif fade_type == "out": t = math.sqrt(t) value = start_vol + (end_vol - start_vol) * t points.append(AutomationPoint(time=time, value=value)) envelope = AutomationEnvelope( parameter_name="Volume", device_name="Mixer", points=points, ) self._envelopes.append(envelope) logger.info("Volume fade %s: %d->%d compases, %.2f->%.2f", fade_type, start_bar, end_bar, start_vol, end_vol) return envelope def automate_delay( self, track_index: int, start_bar: int, end_bar: int, feedback_start: float = DEFAULT_DELAY_FEEDBACK_START, feedback_end: float = DEFAULT_DELAY_FEEDBACK_END, parameter: str = "Feedback" ) -> AutomationEnvelope: """ T029: Automatización de feedback de delay. Args: track_index: Índice de la pista start_bar: Compás inicial end_bar: Compás final feedback_start: Feedback inicial (0.0-1.0) feedback_end: Feedback final (0.0-1.0) parameter: Nombre del parámetro Returns: AutomationEnvelope creado """ start_time = start_bar * 4.0 end_time = end_bar * 4.0 duration = end_time - start_time points = [] num_points = max(4, int(duration / 4)) for i in range(num_points + 1): t = i / num_points time = start_time + t * duration value = feedback_start + (feedback_end - feedback_start) * t points.append(AutomationPoint(time=time, value=value)) envelope = AutomationEnvelope( parameter_name=parameter, device_name="Delay", points=points, ) self._envelopes.append(envelope) logger.info("Delay feedback: %d->%d compases, %.2f->%.2f", start_bar, end_bar, feedback_start, feedback_end) return envelope def automate_send( self, track_index: int, return_index: int, start_bar: int, end_bar: int, start_amount: float = 0.0, end_amount: float = 0.5, send_name: str = "" ) -> AutomationEnvelope: """ T030: Automatización de cantidad de envío (send). Args: track_index: Índice de la pista return_index: Índice del track de retorno start_bar: Compás inicial end_bar: Compás final start_amount: Cantidad inicial (0.0-1.0) end_amount: Cantidad final (0.0-1.0) send_name: Nombre opcional del send Returns: AutomationEnvelope creado """ start_time = start_bar * 4.0 end_time = end_bar * 4.0 duration = end_time - start_time points = [] num_points = max(4, int(duration / 4)) for i in range(num_points + 1): t = i / num_points time = start_time + t * duration value = start_amount + (end_amount - start_amount) * t points.append(AutomationPoint(time=time, value=value)) device_name = send_name if send_name else f"Send {return_index}" envelope = AutomationEnvelope( parameter_name="Send Amount", device_name=device_name, points=points, ) self._envelopes.append(envelope) logger.info("Send automation: %d->%d compases, %.2f->%.2f", start_bar, end_bar, start_amount, end_amount) return envelope def get_all_envelopes(self) -> List[AutomationEnvelope]: """Retorna todos los envelopes creados.""" return self._envelopes.copy() # ============================================================================= # CLASE 3: FX CREATOR (T031-T035) # ============================================================================= class FXCreator: """ Creador de efectos FX para transiciones y énfasis. Genera risers, downlifters, impacts y otros efectos para mejorar las transiciones entre secciones. """ def __init__(self): self._fx_clips: List[ArrangementClip] = [] def create_riser( self, track_index: int, start_bar: int, duration: int = 8, intensity: float = 0.8, pitch_range: Tuple[int, int] = (36, 84) ) -> ArrangementClip: """ T031: Crea un riser pre-drop (crescendo de pitch/tensión). Args: track_index: Índice de la pista start_bar: Compás inicial duration: Duración en compases intensity: Intensidad (0.0-1.0) pitch_range: Rango de notas MIDI (min, max) Returns: ArrangementClip del riser """ start_time = start_bar * 4.0 total_duration = duration * 4.0 # Crear notas que suben de pitch notes = [] num_notes = int(duration * 4 * 2) # 2 notas por beat min_pitch, max_pitch = pitch_range for i in range(num_notes): t = i / num_notes time = start_time + t * total_duration # Pitch ascendente pitch = int(min_pitch + (max_pitch - min_pitch) * t) # Velocity ascendente para más tensión velocity = int(60 + 67 * t * intensity) # Duración más corta al final para staccato effect note_duration = 0.5 - (0.3 * t) notes.append({ "pitch": pitch, "start_time": time, "duration": max(0.1, note_duration), "velocity": min(127, velocity), }) clip = ArrangementClip( name=f"Riser - {duration} bars", track_index=track_index, start_time=start_time, duration=total_duration, is_audio=False, notes=notes, ) self._fx_clips.append(clip) logger.info("Riser creado: %d compases, intensidad %.2f", duration, intensity) return clip def create_downlifter( self, track_index: int, start_bar: int, duration: int = 4, intensity: float = 0.7, pitch_range: Tuple[int, int] = (72, 36) ) -> ArrangementClip: """ T032: Crea un downlifter post-drop (descenso de pitch/tensión). Args: track_index: Índice de la pista start_bar: Compás inicial duration: Duración en compases intensity: Intensidad (0.0-1.0) pitch_range: Rango de notas MIDI (start, end) Returns: ArrangementClip del downlifter """ start_time = start_bar * 4.0 total_duration = duration * 4.0 notes = [] num_notes = int(duration * 4) start_pitch, end_pitch = pitch_range for i in range(num_notes): t = i / num_notes time = start_time + t * total_duration # Pitch descendente pitch = int(start_pitch + (end_pitch - start_pitch) * t) # Velocity descendente velocity = int(100 - 60 * t * intensity) notes.append({ "pitch": pitch, "start_time": time, "duration": 0.5, "velocity": max(1, velocity), }) clip = ArrangementClip( name=f"Downlifter - {duration} bars", track_index=track_index, start_time=start_time, duration=total_duration, is_audio=False, notes=notes, ) self._fx_clips.append(clip) logger.info("Downlifter creado: %d compases, intensidad %.2f", duration, intensity) return clip def create_impact( self, track_index: int, position: Union[int, float], intensity: float = 1.0, impact_type: str = "hit" ) -> ArrangementClip: """ T033: Crea un impact FX (hit, crash, sub drop). Args: track_index: Índice de la pista position: Posición en compases (int) o beats (float) intensity: Intensidad del impacto (0.0-1.0) impact_type: Tipo de impacto ("hit", "crash", "sub_drop", "noise") Returns: ArrangementClip del impact """ if isinstance(position, int): start_time = position * 4.0 else: start_time = position # Configuración según tipo if impact_type == "hit": base_pitch = 36 velocity = int(100 + 27 * intensity) duration = 2.0 elif impact_type == "crash": base_pitch = 49 velocity = int(80 + 47 * intensity) duration = 4.0 elif impact_type == "sub_drop": base_pitch = 24 velocity = int(110 + 17 * intensity) duration = 3.0 else: # noise base_pitch = 60 velocity = int(90 + 37 * intensity) duration = 2.0 notes = [{ "pitch": base_pitch, "start_time": start_time, "duration": duration, "velocity": min(127, velocity), }] clip = ArrangementClip( name=f"Impact {impact_type}", track_index=track_index, start_time=start_time, duration=duration, is_audio=False, notes=notes, ) self._fx_clips.append(clip) logger.info("Impact creado: %s en %.2f, intensidad %.2f", impact_type, position, intensity) return clip def create_silence( self, track_index: int, start_bar: int, duration: int = 1, fade_edges: bool = True ) -> ArrangementClip: """ T034: Crea una barra de silencio (mute momentáneo). Args: track_index: Índice de la pista start_bar: Compás inicial duration: Duración en compases fade_edges: Si se aplican fades en los bordes Returns: ArrangementClip de silencio (como marcador) """ start_time = start_bar * 4.0 total_duration = duration * 4.0 # El silencio se implementa como un clip vacío con metadatos # En la práctica, esto se usa para automatizar el volumen a -inf clip = ArrangementClip( name=f"Silence - {duration} bars", track_index=track_index, start_time=start_time, duration=total_duration, is_audio=False, notes=[], # Sin notas = silencio ) self._fx_clips.append(clip) logger.info("Silencio creado: %d compases desde compás %d", duration, start_bar) return clip def create_fx_automation_section( self, section_type: str, start_bar: int, duration: int, track_indices: Optional[List[int]] = None ) -> List[ArrangementClip]: """ T035: Crea una sección completa de FX según el tipo. Args: section_type: Tipo de sección ("pre_drop", "post_drop", "transition") start_bar: Compás inicial duration: Duración en compases track_indices: Lista de tracks afectados (None = todos) Returns: Lista de ArrangementClips de FX """ clips = [] if track_indices is None: track_indices = [0, 1, 2] # Default tracks if section_type == "pre_drop": # Riser en build for idx in track_indices[:1]: # Solo en primer track de FX clip = self.create_riser(idx, start_bar, duration, intensity=0.9) clips.append(clip) # Impact al final if len(track_indices) > 1: impact = self.create_impact( track_indices[1], start_bar + duration, intensity=1.0, impact_type="hit" ) clips.append(impact) elif section_type == "post_drop": # Downlifter después del drop for idx in track_indices[:1]: clip = self.create_downlifter(idx, start_bar, duration, intensity=0.6) clips.append(clip) elif section_type == "transition": # Swell hacia arriba y luego down half_duration = duration // 2 for idx in track_indices[:1]: # Primera mitad: subida rise = self.create_riser(idx, start_bar, half_duration, intensity=0.7) clips.append(rise) # Segunda mitad: bajada down = self.create_downlifter(idx, start_bar + half_duration, half_duration, intensity=0.5) clips.append(down) logger.info("Sección FX '%s' creada: %d clips", section_type, len(clips)) return clips def get_all_fx_clips(self) -> List[ArrangementClip]: """Retorna todos los clips FX creados.""" return self._fx_clips.copy() # ============================================================================= # CLASE 4: SAMPLE PROCESSOR (T036-T040) # ============================================================================= class SampleProcessor: """ Procesador avanzado de samples. Proporciona funcionalidades para resamplear, revertir, hacer slices, aplicar efectos granulares y crear capas ambientales. """ def __init__(self): self._processed_samples: List[Dict[str, Any]] = [] def resample_track( self, track_index: int, output_track_index: int, start_bar: int = 0, duration_bars: int = 16, output_name: str = "Resampled" ) -> Dict[str, Any]: """ T036: Graba/resamplea un track a un track de audio. Args: track_index: Índice del track a resamplear output_track_index: Índice del track de salida start_bar: Compás de inicio duration_bars: Duración en compases output_name: Nombre del clip resultante Returns: Información del sample resampleado """ start_time = start_bar * 4.0 duration = duration_bars * 4.0 result = { "source_track": track_index, "output_track": output_track_index, "start_time": start_time, "duration": duration, "name": output_name, "status": "configured", "note": "Resampling requiere renderizado en Ableton Live", } self._processed_samples.append(result) logger.info("Resample configurado: track %d -> %d (%d compases)", track_index, output_track_index, duration_bars) return result def reverse_sample( self, sample_path: str, output_path: Optional[str] = None ) -> Dict[str, Any]: """ T037: Carga un sample, lo revierte y guarda nuevo archivo. Args: sample_path: Ruta al sample original output_path: Ruta de salida (None = añade _reversed) Returns: Información del sample revertido """ if not os.path.isfile(sample_path): return {"error": f"Sample no encontrado: {sample_path}"} # Generar nombre de salida si no se proporciona if output_path is None: base, ext = os.path.splitext(sample_path) output_path = f"{base}_reversed{ext}" result = { "original_path": sample_path, "output_path": output_path, "status": "configured", "note": "Reversing requiere procesamiento de audio externo", } self._processed_samples.append(result) logger.info("Reverse configurado: %s", os.path.basename(sample_path)) return result def slice_and_rearrange( self, sample_path: str, num_slices: int = 8, new_pattern: Optional[List[int]] = None ) -> Dict[str, Any]: """ T038: Divide un sample en slices y los rearrangea. Args: sample_path: Ruta al sample num_slices: Número de slices a crear new_pattern: Patrón de rearrange (índices de slices) Returns: Información del sample procesado """ if not os.path.isfile(sample_path): return {"error": f"Sample no encontrado: {sample_path}"} # Si no hay patrón, crear uno aleatorio if new_pattern is None: new_pattern = list(range(num_slices)) random.shuffle(new_pattern) # Calcular puntos de slice (posiciones en beats) # Asumimos un sample de 4 compases por defecto total_beats = 16.0 slice_duration = total_beats / num_slices slices = [] for i in range(num_slices): start = i * slice_duration end = (i + 1) * slice_duration slices.append({ "index": i, "start_beat": start, "end_beat": end, "duration": slice_duration, }) # Crear nuevo orden rearranged = [] for idx in new_pattern: if 0 <= idx < len(slices): rearranged.append(slices[idx].copy()) result = { "original_path": sample_path, "num_slices": num_slices, "slices": slices, "new_pattern": new_pattern, "rearranged": rearranged, "status": "configured", } self._processed_samples.append(result) logger.info("Slice & rearrange: %d slices, patrón %s", num_slices, new_pattern) return result def apply_granular_effect( self, track_index: int, grain_size: float = 0.1, density: float = 0.5, spread: float = 0.3, duration_bars: int = 4 ) -> Dict[str, Any]: """ T039: Aplica efecto granular (simulado con notas MIDI). Args: track_index: Índice del track grain_size: Tamaño de grano en beats density: Densidad de granos (0.0-1.0) spread: Dispersión estéreo/pitch duration_bars: Duración en compases Returns: Información del efecto aplicado """ duration = duration_bars * 4.0 # Crear notas que simulan granos notes = [] current_time = 0.0 while current_time < duration: # Decidir si colocar un grano if random.random() < density: # Pitch aleatorio con spread base_pitch = 60 pitch_variation = int(spread * 24 * (random.random() - 0.5)) pitch = base_pitch + pitch_variation # Velocity aleatoria velocity = int(60 + 40 * random.random()) notes.append({ "pitch": pitch, "start_time": current_time, "duration": grain_size, "velocity": velocity, }) # Avanzar current_time += grain_size * (0.5 + random.random() * 0.5) result = { "track_index": track_index, "grain_size": grain_size, "density": density, "spread": spread, "note_count": len(notes), "notes": notes, "status": "configured", } self._processed_samples.append(result) logger.info("Granular effect: %d notas en %d compases", len(notes), duration_bars) return result def create_ambient_layer( self, chord_progression: List[str], duration: int = 32, base_octave: int = 4, track_name: str = "Ambient Pad" ) -> Dict[str, Any]: """ T040: Crea un track de pad ambiente con progresión armónica. Args: chord_progression: Lista de acordes (ej: ["Am", "F", "C", "G"]) duration: Duración total en compases base_octave: Octava base (4 = C4) track_name: Nombre del track Returns: Configuración del pad ambiente """ # Mapeo de acordes a notas MIDI chord_notes = { "Am": [9, 12, 16], # A, C, E "Dm": [2, 5, 9], # D, F, A "Em": [4, 7, 11], # E, G, B "F": [5, 9, 12], # F, A, C "G": [7, 11, 14], # G, B, D "C": [0, 4, 7], # C, E, G "D": [2, 6, 9], # D, F#, A "E": [4, 8, 11], # E, G#, B "A": [9, 13, 16], # A, C#, E "Bm": [11, 14, 18], # B, D, F# } base_midi = 12 * (base_octave + 1) # C4 = 60 # Calcular compases por acorde bars_per_chord = duration // len(chord_progression) notes = [] current_bar = 0 for chord in chord_progression: intervals = chord_notes.get(chord, [0, 4, 7]) # Crear notas del acorde extendidas for bar in range(bars_per_chord): for beat in range(4): # Notas largas para efecto pad if beat == 0 or random.random() < 0.3: for interval in intervals: pitch = base_midi + interval # Añadir variación de octava if random.random() < 0.2: pitch += 12 note_time = (current_bar + bar) * 4.0 + beat notes.append({ "pitch": pitch, "start_time": note_time, "duration": 2.0 + random.random() * 2.0, "velocity": int(50 + 30 * random.random()), }) current_bar += bars_per_chord result = { "track_name": track_name, "chord_progression": chord_progression, "duration": duration, "note_count": len(notes), "notes": notes, "status": "configured", } self._processed_samples.append(result) logger.info("Ambient pad creado: %d notas, progresión %s", len(notes), chord_progression) return result def get_all_processed(self) -> List[Dict[str, Any]]: """Retorna todos los samples procesados.""" return self._processed_samples.copy() # ============================================================================= # FUNCIONES DE UTILIDAD # ============================================================================= def arrangement_to_dict(arrangement: ArrangementConfig) -> Dict[str, Any]: """ Serializa un ArrangementConfig a diccionario. Args: arrangement: Configuración a serializar Returns: Diccionario con la estructura completa """ return arrangement.to_dict() def dict_to_arrangement(data: Dict[str, Any]) -> ArrangementConfig: """ Deserializa un diccionario a ArrangementConfig. Args: data: Diccionario con la configuración Returns: ArrangementConfig reconstruido """ sections = [] for sec_data in data.get("sections", []): clips = [] for clip_data in sec_data.get("clips", []): clips.append(ArrangementClip( name=clip_data.get("name", ""), track_index=clip_data.get("track_index", 0), start_time=clip_data.get("start_time", 0.0), duration=clip_data.get("duration", 4.0), is_audio=clip_data.get("is_audio", False), sample_path=clip_data.get("sample_path", ""), notes=clip_data.get("notes", []), )) automations = [] for auto_data in sec_data.get("automations", []): points = [ AutomationPoint(time=p["time"], value=p["value"]) for p in auto_data.get("points", []) ] automations.append(AutomationEnvelope( parameter_name=auto_data.get("parameter_name", ""), device_name=auto_data.get("device_name", ""), points=points, )) sections.append(ArrangementSection( name=sec_data.get("name", ""), start_bar=sec_data.get("start_bar", 0), bars=sec_data.get("bars", 8), clips=clips, automations=automations, energy_level=sec_data.get("energy_level", 0.5), )) markers = [ SectionMarker( name=m.get("name", ""), start_bar=m.get("start_bar", 0), end_bar=m.get("end_bar", 8), color=m.get("color", 0), ) for m in data.get("markers", []) ] return ArrangementConfig( total_bars=data.get("total_bars", 64), sections=sections, markers=markers, tempo=data.get("tempo", 95.0), ) def get_arrangement_length(arrangement: ArrangementConfig) -> int: """ Retorna la duración total del arrangement en compases. Args: arrangement: Configuración del arrangement Returns: Duración total en compases """ if arrangement.sections: last_section = arrangement.sections[-1] return last_section.start_bar + last_section.bars return arrangement.total_bars # ============================================================================= # FUNCIONES DE CONVENIENCIA # ============================================================================= def create_full_arrangement( song_config: Dict[str, Any], include_fx: bool = True, include_automation: bool = True ) -> Dict[str, Any]: """ Crea un arrangement completo con todas las características. Args: song_config: Configuración de la canción include_fx: Si incluir efectos FX include_automation: Si incluir automatizaciones Returns: Configuración completa del arrangement """ # 1. Crear estructura base builder = ArrangementBuilder() arrangement = builder.fill_arrangement_with_song(song_config) # 2. Añadir FX si se solicita fx_clips = [] if include_fx: fx_creator = FXCreator() # Buscar secciones build y crear risers for section in arrangement.sections: if "build" in section.name.lower(): fx_clips.extend( fx_creator.create_fx_automation_section( "pre_drop", section.start_bar, section.bars, [len(arrangement.sections)] # Track de FX ) ) elif "break" in section.name.lower(): fx_clips.extend( fx_creator.create_fx_automation_section( "post_drop", section.start_bar, min(4, section.bars), [len(arrangement.sections)] ) ) # 3. Añadir automatizaciones si se solicita automations = [] if include_automation: auto_engine = AutomationEngine() # Automatizar filtros en builds for section in arrangement.sections: if "build" in section.name.lower(): auto_engine.automate_filter( track_index=5, # Bass track típico start_bar=section.start_bar, end_bar=section.start_bar + section.bars, start_freq=400, end_freq=8000, ) return { "arrangement": arrangement.to_dict(), "fx_clips": [c.to_dict() for c in fx_clips], "automations": [a.to_dict() for a in automations], } # ============================================================================= # EXPORTS # ============================================================================= __all__ = [ "ArrangementBuilder", "AutomationEngine", "FXCreator", "SampleProcessor", "ArrangementConfig", "ArrangementSection", "ArrangementClip", "AutomationEnvelope", "SectionMarker", "arrangement_to_dict", "dict_to_arrangement", "get_arrangement_length", "create_full_arrangement", ] # ============================================================================= # MAIN / TEST # ============================================================================= if __name__ == "__main__": logging.basicConfig(level=logging.INFO) print("=" * 70) print("ARRANGEMENT ENGINE - Arrangement View and Automation Engine") print("=" * 70) # Test 1: ArrangementBuilder print("\n1. Testing ArrangementBuilder...") builder = ArrangementBuilder() song_config = { "bpm": 95, "structure": "intro_build_drop_break_outro", "tracks": [ { "name": "Kick", "clips": [ {"name": "Kick Pattern", "start_time": 0, "duration": 64, "notes": []} ] } ] } arrangement = builder.fill_arrangement_with_song(song_config) print(f" Total bars: {arrangement.total_bars}") print(f" Sections: {[s.name for s in arrangement.sections]}") print(f" Markers: {[m.name for m in arrangement.markers]}") # Test 2: AutomationEngine print("\n2. Testing AutomationEngine...") auto = AutomationEngine() env = auto.automate_filter( track_index=0, start_bar=8, end_bar=16, start_freq=200, end_freq=20000, curve="exponential" ) print(f" Filter sweep: {len(env.points)} points") env2 = auto.automate_volume( track_index=0, start_bar=0, end_bar=8, start_vol=0.0, end_vol=0.85, fade_type="in" ) print(f" Volume fade: {len(env2.points)} points") # Test 3: FXCreator print("\n3. Testing FXCreator...") fx = FXCreator() riser = fx.create_riser(track_index=7, start_bar=8, duration=8, intensity=0.9) print(f" Riser: {len(riser.notes)} notes") impact = fx.create_impact(track_index=7, position=16, intensity=1.0) print(f" Impact: note pitch {impact.notes[0]['pitch']}") fx_section = fx.create_fx_automation_section( section_type="pre_drop", start_bar=24, duration=8, track_indices=[7, 8] ) print(f" FX Section: {len(fx_section)} clips") # Test 4: SampleProcessor print("\n4. Testing SampleProcessor...") processor = SampleProcessor() ambient = processor.create_ambient_layer( chord_progression=["Am", "F", "C", "G"], duration=32, base_octave=4 ) print(f" Ambient pad: {ambient['note_count']} notes") granular = processor.apply_granular_effect( track_index=5, grain_size=0.1, density=0.6, spread=0.4, duration_bars=4 ) print(f" Granular effect: {granular['note_count']} grains") slice_result = processor.slice_and_rearrange( sample_path="C:/samples/test.wav", num_slices=8, new_pattern=[3, 1, 7, 0, 2, 5, 4, 6] ) print(f" Slices: {slice_result['num_slices']}, pattern: {slice_result['new_pattern']}") # Test 5: Utilities print("\n5. Testing utilities...") data = arrangement_to_dict(arrangement) print(f" Serialized: {len(data.keys())} keys") restored = dict_to_arrangement(data) print(f" Restored: {len(restored.sections)} sections") length = get_arrangement_length(arrangement) print(f" Total length: {length} bars") # Test 6: Full pipeline print("\n6. Testing full arrangement pipeline...") full = create_full_arrangement(song_config, include_fx=True, include_automation=True) print(f" Full arrangement keys: {list(full.keys())}") print(f" FX clips: {len(full['fx_clips'])}") print("\n" + "=" * 70) print("All tests completed successfully!") print("=" * 70)