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:
75
AbletonMCP_AI/MCP_Server/tests/test_human_feel.py
Normal file
75
AbletonMCP_AI/MCP_Server/tests/test_human_feel.py
Normal file
@@ -0,0 +1,75 @@
|
||||
"""
|
||||
test_human_feel.py - Tests para HumanFeelEngine
|
||||
T101-T103: Unit tests
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import unittest
|
||||
from human_feel import HumanFeelEngine
|
||||
|
||||
|
||||
class TestHumanFeelEngine(unittest.TestCase):
|
||||
"""Tests para HumanFeelEngine"""
|
||||
|
||||
def setUp(self):
|
||||
self.engine = HumanFeelEngine(seed=42)
|
||||
|
||||
def test_timing_variation_range(self):
|
||||
"""T040: Timing variation dentro de rango ±5ms."""
|
||||
notes = [{'pitch': 60, 'start': 0.0, 'velocity': 100}]
|
||||
result = self.engine.apply_timing_variation(notes, amount_ms=5.0)
|
||||
|
||||
for note in result:
|
||||
offset_ms = (note['start'] - 0.0) * 1000
|
||||
self.assertGreaterEqual(offset_ms, -5.0)
|
||||
self.assertLessEqual(offset_ms, 5.0)
|
||||
|
||||
def test_velocity_humanize_variance(self):
|
||||
"""T041: Velocity variation ±5%."""
|
||||
notes = [{'pitch': 60, 'start': 0.0, 'velocity': 100}]
|
||||
result = self.engine.apply_velocity_humanize(notes, variance=0.05)
|
||||
|
||||
for note in result:
|
||||
# Velocity debe estar en rango 95-105
|
||||
self.assertGreaterEqual(note['velocity'], 95)
|
||||
self.assertLessEqual(note['velocity'], 105)
|
||||
|
||||
def test_note_skip_probability(self):
|
||||
"""T042: Probabilidad de skip ~2%."""
|
||||
notes = [{'pitch': 60, 'start': float(i), 'velocity': 100} for i in range(100)]
|
||||
result = self.engine.apply_note_skip_probability(notes, prob=0.02)
|
||||
|
||||
# Con seed=42, debe mantener aprox 98% de notas
|
||||
self.assertGreater(len(result), 90) # No muy estricto por randomness
|
||||
self.assertLess(len(result), 100)
|
||||
|
||||
def test_section_dynamics_scale(self):
|
||||
"""T047-T050: Dinámica por sección."""
|
||||
notes = [{'pitch': 60, 'start': 0.0, 'velocity': 100}]
|
||||
|
||||
# Intro = 70%
|
||||
intro_notes = self.engine.apply_section_dynamics(notes, 'intro')
|
||||
self.assertEqual(intro_notes[0]['velocity'], 70)
|
||||
|
||||
# Drop = 100%
|
||||
drop_notes = self.engine.apply_section_dynamics(notes, 'drop')
|
||||
self.assertEqual(drop_notes[0]['velocity'], 100)
|
||||
|
||||
# Build = 85%
|
||||
build_notes = self.engine.apply_section_dynamics(notes, 'build')
|
||||
self.assertEqual(build_notes[0]['velocity'], 85)
|
||||
|
||||
def test_groove_applies_to_offbeat(self):
|
||||
"""T044-T046: Groove aplica a notas off-beat."""
|
||||
# Nota en off-beat (beat position 0.5)
|
||||
notes = [{'pitch': 60, 'start': 4.5, 'velocity': 100}]
|
||||
result = self.engine.apply_groove(notes, style='shuffle', amount=1.0)
|
||||
|
||||
# Debe tener delay aplicado
|
||||
self.assertGreater(result[0]['start'], 4.5)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
106
AbletonMCP_AI/MCP_Server/tests/test_integration.py
Normal file
106
AbletonMCP_AI/MCP_Server/tests/test_integration.py
Normal file
@@ -0,0 +1,106 @@
|
||||
"""
|
||||
test_integration.py - Tests de integración end-to-end
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import unittest
|
||||
from full_integration import AbletonMCPFullPipeline, generate_complete_track
|
||||
|
||||
|
||||
class TestFullPipeline(unittest.TestCase):
|
||||
"""Tests de integración completa"""
|
||||
|
||||
def setUp(self):
|
||||
self.pipeline = AbletonMCPFullPipeline(seed=42)
|
||||
|
||||
def test_generate_from_vibe_techno(self):
|
||||
"""Test generación desde vibe techno."""
|
||||
result = self.pipeline.generate_from_vibe("dark warehouse techno")
|
||||
|
||||
self.assertEqual(result['genre'], 'techno')
|
||||
self.assertIn('bpm', result)
|
||||
self.assertIn('key', result)
|
||||
self.assertIn('structure', result)
|
||||
self.assertTrue(result['dj_friendly'])
|
||||
|
||||
def test_generate_from_vibe_house(self):
|
||||
"""Test generación desde vibe house."""
|
||||
result = self.pipeline.generate_from_vibe("deep house sunset")
|
||||
|
||||
self.assertEqual(result['genre'], 'house')
|
||||
self.assertIn('bpm', result)
|
||||
self.assertGreaterEqual(result['bpm'], 110)
|
||||
self.assertLessEqual(result['bpm'], 130)
|
||||
|
||||
def test_full_pipeline_applies_human_feel(self):
|
||||
"""Test que human feel está configurado."""
|
||||
result = self.pipeline.generate_from_vibe("techno", apply_full_pipeline=True)
|
||||
|
||||
self.assertIn('human_feel', result)
|
||||
self.assertTrue(result['human_feel']['enabled'])
|
||||
|
||||
def test_full_pipeline_creates_structure(self):
|
||||
"""Test que se crea estructura."""
|
||||
result = self.pipeline.generate_from_vibe("techno")
|
||||
|
||||
self.assertIn('structure', result)
|
||||
self.assertGreater(len(result['structure']), 0)
|
||||
|
||||
def test_full_pipeline_creates_transitions(self):
|
||||
"""Test que se crean transiciones."""
|
||||
result = self.pipeline.generate_from_vibe("techno")
|
||||
|
||||
self.assertIn('transitions', result)
|
||||
self.assertIsInstance(result['transitions'], list)
|
||||
|
||||
def test_full_pipeline_creates_atmos_events(self):
|
||||
"""Test que se detectan gaps y crean atmos."""
|
||||
result = self.pipeline.generate_from_vibe("techno")
|
||||
|
||||
self.assertIn('atmos_events', result)
|
||||
|
||||
def test_full_pipeline_creates_fx_events(self):
|
||||
"""Test que se crean FX automáticos."""
|
||||
result = self.pipeline.generate_from_vibe("techno")
|
||||
|
||||
self.assertIn('fx_events', result)
|
||||
|
||||
def test_full_pipeline_creates_master_chain(self):
|
||||
"""Test que se configura master chain."""
|
||||
result = self.pipeline.generate_from_vibe("techno")
|
||||
|
||||
self.assertIn('master_chain', result)
|
||||
self.assertGreater(len(result['master_chain']), 0)
|
||||
|
||||
def test_generate_complete_track_function(self):
|
||||
"""Test función de conveniencia."""
|
||||
result = generate_complete_track("industrial techno", seed=123)
|
||||
|
||||
self.assertIn('genre', result)
|
||||
self.assertIn('vibe_params', result)
|
||||
|
||||
|
||||
class TestCritiqueAndFix(unittest.TestCase):
|
||||
"""Tests para critique y auto-fix"""
|
||||
|
||||
def setUp(self):
|
||||
self.pipeline = AbletonMCPFullPipeline(seed=42)
|
||||
|
||||
def test_critique_returns_scores(self):
|
||||
"""Test que critique retorna scores."""
|
||||
mock_song = {
|
||||
'sections': [{'name': 'Intro'}, {'name': 'Drop'}],
|
||||
'tracks': [{'name': 'Drums'}, {'name': 'Bass'}]
|
||||
}
|
||||
|
||||
result = self.pipeline.critique_and_fix(mock_song)
|
||||
|
||||
self.assertIn('critique', result)
|
||||
self.assertIn('final_score', result)
|
||||
self.assertIsInstance(result['final_score'], float)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
77
AbletonMCP_AI/MCP_Server/tests/test_sample_selector.py
Normal file
77
AbletonMCP_AI/MCP_Server/tests/test_sample_selector.py
Normal file
@@ -0,0 +1,77 @@
|
||||
"""
|
||||
test_sample_selector.py - Tests para SampleSelector
|
||||
T101-T103: Unit tests
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import unittest
|
||||
from unittest.mock import Mock, MagicMock
|
||||
from sample_selector import SampleSelector, Sample
|
||||
|
||||
|
||||
class TestSampleSelector(unittest.TestCase):
|
||||
"""Tests para SampleSelector"""
|
||||
|
||||
def setUp(self):
|
||||
self.selector = SampleSelector()
|
||||
|
||||
def test_palette_bonus_exact_match(self):
|
||||
"""T026: Bonus 1.4x para folder ancla exacto."""
|
||||
# Simular que tenemos un palette
|
||||
self.selector.set_palette_data({'drums': '/samples/Kicks'})
|
||||
|
||||
# Sample en folder exacto
|
||||
bonus = self.selector._calculate_palette_bonus('/samples/Kicks/kick_01.wav', '/samples/Kicks')
|
||||
self.assertEqual(bonus, 1.4)
|
||||
|
||||
def test_palette_bonus_sibling_folder(self):
|
||||
"""T026: Bonus 1.2x para folder hermano."""
|
||||
self.selector.set_palette_data({'drums': '/samples/Kicks'})
|
||||
|
||||
# Sample en folder hermano
|
||||
bonus = self.selector._calculate_palette_bonus('/samples/Snares/snare_01.wav', '/samples/Kicks')
|
||||
self.assertEqual(bonus, 1.2)
|
||||
|
||||
|
||||
def test_palette_bonus_different_folder(self):
|
||||
"""T026: Penalizacion 0.9x para folder completamente diferente."""
|
||||
self.selector.set_palette_data({'drums': '/Library/Kicks'})
|
||||
|
||||
# Sample en folder completamente diferente (no es hermano)
|
||||
bonus = self.selector._calculate_palette_bonus('/OtherLibrary/Pads/pad.wav', '/Library/Kicks')
|
||||
self.assertEqual(bonus, 0.9)
|
||||
|
||||
def test_role_to_bus_mapping(self):
|
||||
"""Test mapeo de roles a buses."""
|
||||
self.assertEqual(self.selector._role_to_bus('kick'), 'drums')
|
||||
self.assertEqual(self.selector._role_to_bus('bass'), 'bass')
|
||||
self.assertEqual(self.selector._role_to_bus('synth'), 'music')
|
||||
|
||||
def test_fatigue_calculation(self):
|
||||
"""T022: Cálculo correcto de fatiga."""
|
||||
fatigue_data = {
|
||||
'/samples/kick_01.wav': {'kick': {'uses': 5}}
|
||||
}
|
||||
self.selector.set_fatigue_data(fatigue_data)
|
||||
|
||||
# 5 usos = fatiga moderada = 0.50
|
||||
factor = self.selector._get_persistent_fatigue('/samples/kick_01.wav', 'kick')
|
||||
self.assertEqual(factor, 0.50)
|
||||
|
||||
|
||||
class TestSampleValidation(unittest.TestCase):
|
||||
"""Tests para validación de samples"""
|
||||
|
||||
def test_sample_type_detection(self):
|
||||
"""Test detección de tipo de sample."""
|
||||
from audio_analyzer import AudioAnalyzer
|
||||
|
||||
analyzer = AudioAnalyzer(backend="basic")
|
||||
sample_type = analyzer._classify_by_name("Kick_120_BPM.wav")
|
||||
self.assertIn(sample_type.value.lower(), ['kick', 'unknown'])
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user