""" audio_arrangement.py - DJ Arrangement y Estructura T063-T077: Song Structure, Energy Curve, Transitions """ import random import logging from typing import List, Dict, Any, Optional from dataclasses import dataclass logger = logging.getLogger("AudioArrangement") @dataclass class Section: """Representa una sección musical""" name: str kind: str # intro, build, drop, break, outro bars: int energy: float # 0.0 - 1.0 class DJArrangementEngine: """T063-T077: Engine de estructuras DJ-friendly""" # Energy levels por tipo de sección ENERGY_PROFILES = { 'intro': 0.30, 'build': 0.70, 'drop': 1.00, 'break': 0.50, 'outro': 0.20, } def __init__(self, seed: int = 42): self.rng = random.Random(seed) def generate_structure(self, structure_type: str = "standard") -> List[Section]: """ T063-T066: Genera estructura de canción. - standard: 64 bars (Intro 16, Build 16, Drop 16, Break 16, Drop 16, Outro 16) - minimal: 48 bars (Intro 8, Build 8, Drop 16, Break 8, Drop 8, Outro 8) - extended: 128 bars con A/B drop alternation """ if structure_type == "minimal": return [ Section("Intro", "intro", 8, self.ENERGY_PROFILES['intro']), Section("Build 1", "build", 8, self.ENERGY_PROFILES['build']), Section("Drop A", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Break", "break", 8, self.ENERGY_PROFILES['break']), Section("Drop B", "drop", 8, self.ENERGY_PROFILES['drop']), Section("Outro", "outro", 8, self.ENERGY_PROFILES['outro']), ] elif structure_type == "extended": return [ Section("Intro", "intro", 16, self.ENERGY_PROFILES['intro']), Section("Build 1", "build", 16, self.ENERGY_PROFILES['build']), Section("Drop A", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Break 1", "break", 16, self.ENERGY_PROFILES['break']), Section("Build 2", "build", 16, self.ENERGY_PROFILES['build']), Section("Drop B", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Break 2", "break", 16, self.ENERGY_PROFILES['break']), Section("Build 3", "build", 16, self.ENERGY_PROFILES['build']), Section("Drop C", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Outro", "outro", 16, self.ENERGY_PROFILES['outro']), ] else: # standard return [ Section("Intro", "intro", 16, self.ENERGY_PROFILES['intro']), Section("Build 1", "build", 16, self.ENERGY_PROFILES['build']), Section("Drop A", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Break", "break", 16, self.ENERGY_PROFILES['break']), Section("Drop B", "drop", 16, self.ENERGY_PROFILES['drop']), Section("Outro", "outro", 16, self.ENERGY_PROFILES['outro']), ] def is_dj_friendly(self, structure: List[Section]) -> bool: """Verifica si la estructura es DJ-friendly (intro/outro ≥16 beats).""" if not structure: return False intro = structure[0] outro = structure[-1] # 16 bars = 64 beats return intro.bars >= 4 and outro.bars >= 4 def get_energy_at_position(self, structure: List[Section], bar: int) -> float: """T067-T070: Retorna nivel de energía en posición específica.""" current_bar = 0 for section in structure: if current_bar <= bar < current_bar + section.bars: return section.energy current_bar += section.bars return 0.0 def generate_energy_automation(self, structure: List[Section]) -> List[Dict]: """Genera curva de automatización de energía.""" automation = [] current_bar = 0 for section in structure: automation.append({ 'bar': current_bar, 'energy': section.energy, 'section': section.name }) current_bar += section.bars return automation class TransitionEngine: """T071-T077: Engine de transiciones automáticas""" def __init__(self): self.logger = logging.getLogger("TransitionEngine") def auto_riser(self, section_start: float, n_beats: int = 8) -> Dict: """T071: Auto-riser N beats antes de drop.""" return { 'type': 'riser', 'trigger_at': max(0, section_start - n_beats), 'duration': n_beats, 'intensity': 'build', 'auto_trigger': True } def auto_snare_roll(self, section_start: float, duration_beats: int = 4) -> Dict: """T072: Snare roll automático.""" return { 'type': 'snare_roll', 'trigger_at': max(0, section_start - duration_beats), 'duration': duration_beats, 'pattern': '1/16 notes', 'velocity_ramp': True } def auto_filter_sweep(self, section_start: float, section_end: float, direction: str = "up") -> Dict: """T073: Filter sweep en breaks.""" return { 'type': 'filter_sweep', 'direction': direction, 'start_at': section_start, 'end_at': section_end, 'filter_type': 'lowpass', 'target_freq': 20000 if direction == 'up' else 200 } def auto_downlifter(self, build_section_end: float, drop_section_start: float) -> Dict: """T074: Downlifter en build→drop.""" gap = drop_section_start - build_section_end return { 'type': 'downlifter', 'trigger_at': build_section_end, 'duration': min(2.0, gap) if gap > 0 else 2.0, 'sync_to_drop': True } def auto_fill(self, section_end: float, density: str = 'medium') -> Dict: """T075: Drum fill automático.""" fill_beats = {'low': 1, 'medium': 2, 'high': 4}.get(density, 2) return { 'type': 'drum_fill', 'trigger_at': max(0, section_end - fill_beats), 'duration': fill_beats, 'density': density } def generate_all_transitions(self, structure: List[Section]) -> List[Dict]: """T076-T077: Genera todas las transiciones para la estructura.""" events = [] current_bar = 0 for i, section in enumerate(structure): section_start = current_bar * 4 # Convert bars to beats section_end = section_start + (section.bars * 4) if section.kind == 'drop': # Riser + snare roll antes de drop events.append(self.auto_riser(section_start, 8)) events.append(self.auto_snare_roll(section_start, 4)) if section.kind == 'break': # Filter sweep durante break events.append(self.auto_filter_sweep(section_start, section_end, 'up')) if section.kind == 'build' and i + 1 < len(structure): next_section = structure[i + 1] if next_section.kind == 'drop': # Downlifter build→drop events.append(self.auto_downlifter(section_end, section_end + 1)) # Drum fill al final de secciones intensas if section.kind in ['drop', 'build']: events.append(self.auto_fill(section_end, 'medium')) current_bar += section.bars return events