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>
This commit is contained in:
183
AbletonMCP_AI/MCP_Server/audio_soundscape.py
Normal file
183
AbletonMCP_AI/MCP_Server/audio_soundscape.py
Normal file
@@ -0,0 +1,183 @@
|
||||
"""
|
||||
audio_soundscape.py - Soundscape y FX automáticos
|
||||
T051-T062: Ambiente, FX Bus y Tonal Conflict Detection
|
||||
"""
|
||||
import logging
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from pathlib import Path
|
||||
|
||||
logger = logging.getLogger("AudioSoundscape")
|
||||
|
||||
class SoundscapeEngine:
|
||||
"""T051-T054: Engine de ambientes y texturas"""
|
||||
|
||||
def __init__(self):
|
||||
self.atmos_templates = {
|
||||
'intro': ['*Atmos*Intro*.wav', '*Texture*Intro*.wav', '*Pad*Intro*.wav'],
|
||||
'break': ['*Atmos*Break*.wav', '*Texture*Break*.wav', '*Pad*Break*.wav'],
|
||||
'outro': ['*Atmos*Outro*.wav', '*Texture*Outro*.wav', '*Pad*Outro*.wav'],
|
||||
}
|
||||
|
||||
def detect_ambience_gaps(self, timeline: List[Dict], min_gap_beats: float = 8.0) -> List[Dict]:
|
||||
"""T051: Detecta espacios vacíos sin audio."""
|
||||
gaps = []
|
||||
for i in range(len(timeline) - 1):
|
||||
current_end = timeline[i].get('end', 0)
|
||||
next_start = timeline[i + 1].get('start', current_end)
|
||||
gap = next_start - current_end
|
||||
if gap >= min_gap_beats:
|
||||
gaps.append({
|
||||
'start': current_end,
|
||||
'end': next_start,
|
||||
'duration': gap,
|
||||
'section': timeline[i].get('kind', 'unknown')
|
||||
})
|
||||
return gaps
|
||||
|
||||
def fill_with_atmos(self, gaps: List[Dict], genre: str, key: str) -> List[Dict]:
|
||||
"""T052-T053: Carga atmos loops en gaps detectados."""
|
||||
atmos_events = []
|
||||
for gap in gaps:
|
||||
section = gap.get('section', 'intro')
|
||||
templates = self.atmos_templates.get(section, self.atmos_templates['break'])
|
||||
atmos_events.append({
|
||||
'position': gap['start'],
|
||||
'duration': min(gap['duration'], 16.0), # Max 16 beats
|
||||
'templates': templates,
|
||||
'genre': genre,
|
||||
'key': key,
|
||||
'type': 'atmos_fill'
|
||||
})
|
||||
return atmos_events
|
||||
|
||||
|
||||
class FXEngine:
|
||||
"""T055-T058: Engine de FX automáticos"""
|
||||
|
||||
def __init__(self):
|
||||
self.fx_patterns = {
|
||||
'riser': {'template': '*Riser*.wav', 'pre_beats': 8},
|
||||
'downlifter': {'template': '*Downlifter*.wav', 'post_beats': 2},
|
||||
'impact': {'template': '*Impact*.wav', 'at_position': True},
|
||||
'crash': {'template': '*Crash*.wav', 'at_position': True},
|
||||
'snare_roll': {'template': '*Snare Roll*.wav', 'pre_beats': 4},
|
||||
}
|
||||
|
||||
def auto_riser_before_drop(self, section_start: float, n_beats: int = 8) -> Optional[Dict]:
|
||||
"""T055: Genera riser N beats antes de drop."""
|
||||
return {
|
||||
'type': 'riser',
|
||||
'position': max(0, section_start - n_beats),
|
||||
'duration': n_beats,
|
||||
'template': self.fx_patterns['riser']['template']
|
||||
}
|
||||
|
||||
def auto_downlifter_transition(self, from_section: str, to_section: str,
|
||||
section_end: float) -> Optional[Dict]:
|
||||
"""T056: Auto-downlifter en transiciones."""
|
||||
if to_section in ['drop', 'break'] and from_section in ['build', 'drop']:
|
||||
return {
|
||||
'type': 'downlifter',
|
||||
'position': section_end - 2,
|
||||
'duration': 2,
|
||||
'template': self.fx_patterns['downlifter']['template']
|
||||
}
|
||||
return None
|
||||
|
||||
def auto_impact_on_downbeat(self, section_start: float, section_kind: str) -> Optional[Dict]:
|
||||
"""T057: Impact/crash en downbeats de drop."""
|
||||
if section_kind in ['drop', 'build']:
|
||||
return {
|
||||
'type': 'impact',
|
||||
'position': section_start,
|
||||
'template': self.fx_patterns['impact']['template']
|
||||
}
|
||||
return None
|
||||
|
||||
def auto_snare_roll(self, section_start: float, duration_beats: int = 4) -> Optional[Dict]:
|
||||
"""T058: Snare roll automático antes de drops."""
|
||||
return {
|
||||
'type': 'snare_roll',
|
||||
'position': max(0, section_start - duration_beats),
|
||||
'duration': duration_beats,
|
||||
'template': self.fx_patterns['snare_roll']['template']
|
||||
}
|
||||
|
||||
|
||||
class TonalAnalyzer:
|
||||
"""T059-T062: Análisis de conflictos tonales"""
|
||||
|
||||
NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||||
|
||||
def detect_key_conflict(self, samples: List[Dict], target_key: str) -> List[Dict]:
|
||||
"""T059: Detecta si samples tienen key conflict con target_key."""
|
||||
conflicts = []
|
||||
for sample in samples:
|
||||
sample_key = sample.get('key', '')
|
||||
if sample_key and sample_key != target_key:
|
||||
# Check compatibility using circle of fifths
|
||||
distance = self._key_distance(target_key, sample_key)
|
||||
if distance > 2: # More than 2 steps on circle
|
||||
conflicts.append({
|
||||
'sample': sample.get('path', 'unknown'),
|
||||
'sample_key': sample_key,
|
||||
'target_key': target_key,
|
||||
'distance': distance,
|
||||
'severity': 'high' if distance > 4 else 'medium'
|
||||
})
|
||||
return conflicts
|
||||
|
||||
def _key_distance(self, key1: str, key2: str) -> int:
|
||||
"""Calcula distancia en círculo de quintas."""
|
||||
# Normalize keys
|
||||
is_minor1 = 'm' in key1.lower()
|
||||
is_minor2 = 'm' in key2.lower()
|
||||
|
||||
if is_minor1 != is_minor2:
|
||||
return 6 # Different modes = max distance
|
||||
|
||||
root1 = key1.replace('m', '').replace('M', '')
|
||||
root2 = key2.replace('m', '').replace('M', '')
|
||||
|
||||
try:
|
||||
idx1 = self.NOTE_NAMES.index(root1)
|
||||
idx2 = self.NOTE_NAMES.index(root2)
|
||||
except ValueError:
|
||||
return 6 # Unknown note
|
||||
|
||||
# Distance on circle of fifths
|
||||
circle_of_fifths = [0, 7, 2, 9, 4, 11, 6, 1, 8, 3, 10, 5] # Perfect fifths order
|
||||
pos1 = circle_of_fifths.index(idx1) if idx1 in circle_of_fifths else 0
|
||||
pos2 = circle_of_fifths.index(idx2) if idx2 in circle_of_fifths else 0
|
||||
|
||||
return min(abs(pos1 - pos2), 12 - abs(pos1 - pos2))
|
||||
|
||||
def suggest_transpose(self, sample_path: str, from_key: str, to_key: str) -> int:
|
||||
"""T060-T061: Sugiere semitonos para transponer sample a key objetivo."""
|
||||
try:
|
||||
root_from = from_key.replace('m', '').replace('M', '')
|
||||
root_to = to_key.replace('m', '').replace('M', '')
|
||||
|
||||
idx_from = self.NOTE_NAMES.index(root_from)
|
||||
idx_to = self.NOTE_NAMES.index(root_to)
|
||||
|
||||
semitones = idx_to - idx_from
|
||||
# Normalize to -6 to +6 range
|
||||
if semitones > 6:
|
||||
semitones -= 12
|
||||
elif semitones < -6:
|
||||
semitones += 12
|
||||
|
||||
return semitones
|
||||
except ValueError:
|
||||
return 0 # Can't calculate
|
||||
|
||||
def generate_dissonance_alert(self, conflicts: List[Dict]) -> str:
|
||||
"""T062: Genera alertas de disonancia."""
|
||||
if not conflicts:
|
||||
return "No tonal conflicts detected."
|
||||
|
||||
high_conflicts = [c for c in conflicts if c['severity'] == 'high']
|
||||
if high_conflicts:
|
||||
return f"WARNING: {len(high_conflicts)} high-severity key conflicts detected!"
|
||||
return f"INFO: {len(conflicts)} minor key variations (acceptable)."
|
||||
Reference in New Issue
Block a user