Files
ableton-mcp-ai/AbletonMCP_AI/MCP_Server/audio_arrangement.py
renato97 4332ff65da Implement FASE 3, 4, 6 - 15 new MCP tools, 76/110 tasks complete
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>
2026-03-29 00:59:24 -03:00

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