AVANCES CLAVE: ✅ B001 FIX: MIDI instruments cargan correctamente (Wavetable/Operator) ✅ API fix: app.view.selected_track → self._song.view.selected_track ✅ clear_project: Nuevo comando para limpiar Session + Arrangement View ✅ Drum loop + Harmony: 100bpm gata con progresión Am-F-C-G funcionando ✅ 13 scenes production: Sistema completo operativo Estado: MUY FELIZ, todo funciona perfectamente 🚀
180 lines
7.4 KiB
Python
180 lines
7.4 KiB
Python
#!/usr/bin/env python
|
|
"""Script to add Fases 11-15 advanced sample picker to __init__.py"""
|
|
|
|
import sys
|
|
|
|
def main():
|
|
filepath = r'C:\ProgramData\Ableton\Live 12 Suite\Resources\MIDI Remote Scripts\AbletonMCP_AI\__init__.py'
|
|
|
|
# Read the file
|
|
with open(filepath, 'r', encoding='utf-8') as f:
|
|
content = f.read()
|
|
|
|
# Check if already exists
|
|
if '_pick_for_scene_advanced' in content:
|
|
print("ERROR: _pick_for_scene_advanced already exists!")
|
|
return 1
|
|
|
|
# The old function to find
|
|
old_function = ''' def _pick_for_scene(all_samples, scene_idx, total_scenes):
|
|
"""Distribute samples across scenes so each gets a different one."""
|
|
if not all_samples:
|
|
return None
|
|
if len(all_samples) <= total_scenes:
|
|
return all_samples[scene_idx % len(all_samples)]
|
|
step = len(all_samples) / total_scenes
|
|
idx = int(scene_idx * step) % len(all_samples)
|
|
return all_samples[idx]
|
|
|
|
# Sort drum loops by BPM proximity to tempo'''
|
|
|
|
# The new function to add
|
|
new_function = ''' def _pick_for_scene(all_samples, scene_idx, total_scenes):
|
|
"""Distribute samples across scenes so each gets a different one."""
|
|
if not all_samples:
|
|
return None
|
|
if len(all_samples) <= total_scenes:
|
|
return all_samples[scene_idx % len(all_samples)]
|
|
step = len(all_samples) / total_scenes
|
|
idx = int(scene_idx * step) % len(all_samples)
|
|
return all_samples[idx]
|
|
|
|
# ================================================================
|
|
# FASES 11-15: SISTEMA AVANZADO DE VARIACION MASIVA DE KICKS Y SNARES
|
|
# ================================================================
|
|
# Track samples used in previous scene to avoid repetition
|
|
_prev_scene_samples = {"kicks": [], "snares": []}
|
|
_scene_sample_usage = {"kicks": {}, "snares": {}} # Track usage count per sample
|
|
_all_kicks_used = [] # Track order of all kicks used
|
|
_all_snares_used = [] # Track order of all snares used
|
|
|
|
def _pick_for_scene_advanced(all_samples, scene_idx, total_scenes, energy, prev_samples, sample_type="kick"):
|
|
"""
|
|
Advanced sample selection with energy-based filtering and no-repetition policy.
|
|
|
|
Args:
|
|
all_samples: List of all available sample paths
|
|
scene_idx: Current scene index
|
|
total_scenes: Total number of scenes
|
|
energy: Energy level (0.0-1.0)
|
|
prev_samples: List of samples used in previous scene
|
|
sample_type: "kick" or "snare" for logging
|
|
|
|
Returns:
|
|
Selected sample path or None
|
|
"""
|
|
if not all_samples:
|
|
return None
|
|
|
|
# Energy-based keyword filtering
|
|
soft_keywords = ["soft", "light", "minimal", "gentle", "quiet", "smooth"]
|
|
hard_keywords = ["hard", "heavy", "punch", "kick", "strong", "aggressive", "tight", "solid"]
|
|
|
|
# Filter samples based on energy level
|
|
if energy < 0.3:
|
|
# Low energy: prefer soft/light samples
|
|
filtered = [s for s in all_samples if any(kw in s.lower() for kw in soft_keywords)]
|
|
selection_pool = filtered if filtered else all_samples
|
|
elif energy > 0.8:
|
|
# High energy: prefer hard/heavy/punch samples
|
|
filtered = [s for s in all_samples if any(kw in s.lower() for kw in hard_keywords)]
|
|
selection_pool = filtered if filtered else all_samples
|
|
else:
|
|
# Medium energy: use all samples
|
|
selection_pool = all_samples
|
|
|
|
# Remove samples used in previous scene (no repetition policy)
|
|
available = [s for s in selection_pool if s not in prev_samples]
|
|
|
|
# If not enough samples after filtering, fall back to all samples (excluding prev)
|
|
if len(available) < 1:
|
|
available = [s for s in all_samples if s not in prev_samples]
|
|
|
|
# If still no samples (all were used in prev), use full pool
|
|
if not available:
|
|
available = all_samples
|
|
|
|
# Select sample with least usage count for even rotation
|
|
min_usage = float('inf')
|
|
best_candidates = []
|
|
|
|
usage_dict = _scene_sample_usage.get(sample_type, {})
|
|
for sample in available:
|
|
usage_count = usage_dict.get(sample, 0)
|
|
if usage_count < min_usage:
|
|
min_usage = usage_count
|
|
best_candidates = [sample]
|
|
elif usage_count == min_usage:
|
|
best_candidates.append(sample)
|
|
|
|
# Pick first from best candidates (they have equal lowest usage)
|
|
selected = best_candidates[0] if best_candidates else available[0] if available else None
|
|
|
|
if selected:
|
|
# Update usage tracking
|
|
usage_dict[selected] = usage_dict.get(selected, 0) + 1
|
|
_scene_sample_usage[sample_type] = usage_dict
|
|
|
|
# Track global usage order
|
|
if sample_type == "kick" and selected not in _all_kicks_used:
|
|
_all_kicks_used.append(selected)
|
|
elif sample_type == "snare" and selected not in _all_snares_used:
|
|
_all_snares_used.append(selected)
|
|
|
|
return selected
|
|
|
|
def _get_velocity_for_energy(energy, drum_type="kick"):
|
|
"""
|
|
Get velocity range based on energy level and drum type.
|
|
|
|
Args:
|
|
energy: Energy level (0.0-1.0)
|
|
drum_type: "kick" or "snare"
|
|
|
|
Returns:
|
|
Tuple of (min_velocity, max_velocity)
|
|
"""
|
|
if energy < 0.4:
|
|
# Low energy: softer velocities
|
|
if drum_type == "kick":
|
|
return (70, 80)
|
|
else: # snare
|
|
return (65, 75)
|
|
elif energy <= 0.7:
|
|
# Medium energy
|
|
if drum_type == "kick":
|
|
return (85, 85) # Fixed at 85
|
|
else: # snare
|
|
return (80, 80) # Fixed at 80
|
|
else:
|
|
# High energy: loud velocities
|
|
if drum_type == "kick":
|
|
return (95, 110)
|
|
else: # snare
|
|
return (90, 100)
|
|
|
|
# Sort drum loops by BPM proximity to tempo'''
|
|
|
|
if old_function not in content:
|
|
print("ERROR: Could not find the old function!")
|
|
# Try to find it
|
|
idx = content.find('def _pick_for_scene')
|
|
if idx >= 0:
|
|
print(f"Found at position {idx}")
|
|
print("Context:", repr(content[idx:idx+300]))
|
|
return 1
|
|
|
|
# Replace
|
|
new_content = content.replace(old_function, new_function)
|
|
|
|
# Write back
|
|
with open(filepath, 'w', encoding='utf-8') as f:
|
|
f.write(new_content)
|
|
|
|
print("SUCCESS: Added _pick_for_scene_advanced and _get_velocity_for_energy")
|
|
print(f"File size changed from {len(content)} to {len(new_content)}")
|
|
return 0
|
|
|
|
if __name__ == '__main__':
|
|
sys.exit(main())
|