FASE 3 - Human Feel & Dynamics (10/11 tasks): - apply_clip_fades() - T041: Fade automation per section - write_volume_automation() - T042: Curves (linear, exp, s_curve, punch) - apply_sidechain_pump() - T045: Sidechain by intensity/style - inject_pattern_fills() - T048: Snare rolls, fills by density - humanize_set() - T050: Timing + velocity + groove automation FASE 4 - Key Compatibility & Tonal (9/12 tasks): - audio_key_compatibility.py: Full KEY_COMPATIBILITY_MATRIX - analyze_key_compatibility() - T053: Harmonic compatibility scoring - suggest_key_change() - T054: Circle of fifths modulation - validate_sample_key() - T055: Sample key validation - analyze_spectral_fit() - T057/T062: Spectral role matching FASE 6 - Mastering & QA (8/13 tasks): - calibrate_gain_staging() - T079: Auto gain by bus targets - run_mix_quality_check() - T085: LUFS, peaks, L/R balance - export_stem_mixdown() - T087: 24-bit/44.1kHz stem export New files: - audio_key_compatibility.py (T052) - bus_routing_fix.py (T101-T104) - validation_system_fix.py (T105-T106) Total: 76/110 tasks (69%), 71 MCP tools exposed Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
198 lines
7.5 KiB
Python
198 lines
7.5 KiB
Python
"""
|
|
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
|