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>
76 lines
2.6 KiB
Python
76 lines
2.6 KiB
Python
"""
|
|
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()
|