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>
279 lines
8.9 KiB
Python
279 lines
8.9 KiB
Python
"""
|
|
bus_routing_fix.py - Fix de enrutamiento de buses
|
|
T101-T104: Bus Routing System Fix
|
|
|
|
Problemas a resolver:
|
|
- Drums van a drum rack pero también a master
|
|
- FX no llegan a los returns correctos
|
|
- Vocal chops en bus de FX en lugar de Vocal
|
|
"""
|
|
import logging
|
|
from typing import Dict, Any, List, Optional
|
|
from dataclasses import dataclass
|
|
|
|
logger = logging.getLogger("BusRoutingFix")
|
|
|
|
|
|
@dataclass
|
|
class BusRoute:
|
|
"""Definición de ruta de bus"""
|
|
source_track: str
|
|
target_bus: str
|
|
send_level: float = 0.0 # 0.0 = no send, 1.0 = full send
|
|
should_go_to_master: bool = True
|
|
|
|
|
|
class BusRoutingRules:
|
|
"""T101: Reglas de enrutamiento por tipo de track"""
|
|
|
|
# Mapeo de roles a buses
|
|
ROLE_TO_BUS = {
|
|
'kick': 'drums',
|
|
'clap': 'drums',
|
|
'snare': 'drums',
|
|
'hat': 'drums',
|
|
'perc': 'drums',
|
|
'ride': 'drums',
|
|
'top_loop': 'drums',
|
|
'drum_loop': 'drums',
|
|
'breakbeat': 'drums',
|
|
'sub_bass': 'bass',
|
|
'bass': 'bass',
|
|
'bass_loop': 'bass',
|
|
'chords': 'music',
|
|
'pad': 'music',
|
|
'pluck': 'music',
|
|
'arp': 'music',
|
|
'lead': 'music',
|
|
'counter': 'music',
|
|
'synth': 'music',
|
|
'vocal': 'vocal',
|
|
'vocal_chop': 'vocal',
|
|
'vox': 'vocal',
|
|
'voice': 'vocal',
|
|
'riser': 'fx',
|
|
'downlifter': 'fx',
|
|
'impact': 'fx',
|
|
'crash': 'fx',
|
|
'atmos': 'fx',
|
|
'reverse_fx': 'fx',
|
|
'texture': 'fx',
|
|
}
|
|
|
|
# Buses RCA disponibles
|
|
RCA_BUSES = ['drums', 'bass', 'music', 'vocal', 'fx']
|
|
|
|
# Returns configurados en Live
|
|
RETURN_TRACKS = ['Reverb', 'Delay', 'Chorus', 'Spatial']
|
|
|
|
@classmethod
|
|
def get_bus_for_role(cls, role: str) -> str:
|
|
"""Retorna el bus RCA apropiado para un rol."""
|
|
role_lower = role.lower().replace('_loop', '').replace('loop_', '')
|
|
|
|
# Check direct match
|
|
if role_lower in cls.ROLE_TO_BUS:
|
|
return cls.ROLE_TO_BUS[role_lower]
|
|
|
|
# Check partial match
|
|
for key, bus in cls.ROLE_TO_BUS.items():
|
|
if key in role_lower or role_lower in key:
|
|
return bus
|
|
|
|
# Default por categoría
|
|
if any(d in role_lower for d in ['drum', 'kick', 'snare', 'hat', 'perc']):
|
|
return 'drums'
|
|
if any(b in role_lower for b in ['bass', 'sub', '808', 'low']):
|
|
return 'bass'
|
|
if any(s in role_lower for s in ['synth', 'pad', 'chord', 'lead', 'pluck', 'melody']):
|
|
return 'music'
|
|
if any(v in role_lower for v in ['vocal', 'vox', 'voice', 'chant']):
|
|
return 'vocal'
|
|
if any(f in role_lower for f in ['fx', 'riser', 'impact', 'atmos', 'texture', 'noise']):
|
|
return 'fx'
|
|
|
|
return 'music' # Default fallback
|
|
|
|
|
|
class BusRoutingFixer:
|
|
"""T102-T104: Aplica fixes de enrutamiento"""
|
|
|
|
def __init__(self):
|
|
self.rules = BusRoutingRules()
|
|
self.issues_found: List[Dict] = []
|
|
self.fixes_applied: List[Dict] = []
|
|
|
|
def diagnose_routing(self, tracks_data: List[Dict]) -> List[Dict]:
|
|
"""
|
|
T102: Diagnostica problemas de enrutamiento.
|
|
|
|
Args:
|
|
tracks_data: Lista de tracks con sus configuraciones
|
|
|
|
Returns:
|
|
Lista de problemas encontrados
|
|
"""
|
|
issues = []
|
|
|
|
for track in tracks_data:
|
|
track_name = track.get('name', 'Unknown')
|
|
track_role = track.get('role', '')
|
|
current_bus = track.get('output_bus', 'master')
|
|
|
|
# Determinar bus correcto
|
|
correct_bus = self.rules.get_bus_for_role(track_role or track_name)
|
|
|
|
# Verificar si está en bus incorrecto
|
|
if current_bus != correct_bus and current_bus != 'master':
|
|
issues.append({
|
|
'track': track_name,
|
|
'role': track_role,
|
|
'current_bus': current_bus,
|
|
'correct_bus': correct_bus,
|
|
'issue': 'wrong_bus',
|
|
'severity': 'high' if correct_bus != 'music' else 'medium'
|
|
})
|
|
|
|
# Verificar sends incorrectos (ej: drums enviando a reverb fuerte)
|
|
sends = track.get('sends', {})
|
|
if track_role in ['kick', 'sub_bass']:
|
|
reverb_send = sends.get('Reverb', 0)
|
|
if reverb_send > 0.3:
|
|
issues.append({
|
|
'track': track_name,
|
|
'role': track_role,
|
|
'issue': 'excessive_reverb_on_low',
|
|
'current_send': reverb_send,
|
|
'recommended': 0.1,
|
|
'severity': 'medium'
|
|
})
|
|
|
|
# Verificar que FX tracks no van a master directo
|
|
if correct_bus == 'fx' and track.get('audio_output') == 'Master':
|
|
issues.append({
|
|
'track': track_name,
|
|
'role': track_role,
|
|
'issue': 'fx_to_master_bypass',
|
|
'severity': 'low'
|
|
})
|
|
|
|
self.issues_found = issues
|
|
return issues
|
|
|
|
def apply_routing_fixes(self, ableton_connection, tracks_data: List[Dict]) -> Dict:
|
|
"""
|
|
T103: Aplica fixes de enrutamiento en Ableton.
|
|
|
|
Args:
|
|
ableton_connection: Conexión a Ableton Live
|
|
tracks_data: Datos de tracks a corregir
|
|
|
|
Returns:
|
|
Reporte de fixes aplicados
|
|
"""
|
|
fixes = []
|
|
|
|
for track in tracks_data:
|
|
track_name = track.get('name')
|
|
track_index = track.get('index')
|
|
track_role = track.get('role', '')
|
|
|
|
# Determinar bus correcto
|
|
correct_bus = self.rules.get_bus_for_role(track_role or track_name)
|
|
|
|
try:
|
|
# 1. Cambiar output del track al bus RCA
|
|
# Esto requiere que los buses RCA existan como tracks de audio
|
|
self._set_track_output(ableton_connection, track_index, correct_bus)
|
|
|
|
# 2. Ajustar sends si es necesario
|
|
if track_role in ['kick', 'sub_bass']:
|
|
self._adjust_send(ableton_connection, track_index, 'Reverb', 0.1)
|
|
|
|
fixes.append({
|
|
'track': track_name,
|
|
'action': f'routed_to_{correct_bus}',
|
|
'success': True
|
|
})
|
|
|
|
except Exception as e:
|
|
fixes.append({
|
|
'track': track_name,
|
|
'action': 'routing_fix',
|
|
'success': False,
|
|
'error': str(e)
|
|
})
|
|
|
|
self.fixes_applied = fixes
|
|
return {
|
|
'total_tracks': len(tracks_data),
|
|
'fixes_applied': len([f for f in fixes if f.get('success')]),
|
|
'fixes_failed': len([f for f in fixes if not f.get('success')]),
|
|
'details': fixes
|
|
}
|
|
|
|
def _set_track_output(self, ableton_connection, track_index: int, output_bus: str):
|
|
"""Setea output de un track a un bus específico."""
|
|
# Comando MCP para cambiar output
|
|
cmd = {
|
|
'command': 'set_track_output',
|
|
'track_index': track_index,
|
|
'output': output_bus
|
|
}
|
|
ableton_connection.send_command(cmd)
|
|
|
|
def _adjust_send(self, ableton_connection, track_index: int, send_name: str, level: float):
|
|
"""Ajusta nivel de send."""
|
|
cmd = {
|
|
'command': 'set_send_level',
|
|
'track_index': track_index,
|
|
'send_name': send_name,
|
|
'level': level
|
|
}
|
|
ableton_connection.send_command(cmd)
|
|
|
|
def validate_routing(self, tracks_data: List[Dict]) -> Dict:
|
|
"""
|
|
T104: Valida que el enrutamiento esté correcto.
|
|
|
|
Returns:
|
|
Reporte de validación
|
|
"""
|
|
issues = self.diagnose_routing(tracks_data)
|
|
|
|
critical = [i for i in issues if i.get('severity') == 'high']
|
|
warnings = [i for i in issues if i.get('severity') in ['medium', 'low']]
|
|
|
|
return {
|
|
'valid': len(critical) == 0,
|
|
'critical_issues': len(critical),
|
|
'warnings': len(warnings),
|
|
'total_issues': len(issues),
|
|
'issues': issues
|
|
}
|
|
|
|
def get_bus_routing_config(self) -> Dict[str, Any]:
|
|
"""Retorna configuración completa de enrutamiento."""
|
|
return {
|
|
'buses': self.rules.RCA_BUSES,
|
|
'returns': self.rules.RETURN_TRACKS,
|
|
'role_mapping': self.rules.ROLE_TO_BUS,
|
|
'validation_rules': {
|
|
'kick_reverb_max': 0.1,
|
|
'sub_bass_reverb_max': 0.05,
|
|
'drums_to_fx_send': 0.0,
|
|
}
|
|
}
|
|
|
|
|
|
# Instancia global
|
|
_routing_fixer: Optional[BusRoutingFixer] = None
|
|
|
|
|
|
def get_routing_fixer() -> BusRoutingFixer:
|
|
"""Obtiene instancia global del fixer."""
|
|
global _routing_fixer
|
|
if _routing_fixer is None:
|
|
_routing_fixer = BusRoutingFixer()
|
|
return _routing_fixer
|