#!/usr/bin/env python3 """ Generador CORREGIDO de archivos .als (Ableton Live Set) Versión 100% compatible basada en ingeniería inversa del archivo original. """ import gzip import xml.etree.ElementTree as ET import random import os import shutil from typing import Dict, List, Any class ALSGeneratorFixed: def __init__(self): self.template_file = "jukeblocks - Pop.als" self.output_file = None self.tree = None self.root = None def load_template(self): """Cargar archivo original como plantilla""" try: with gzip.open(self.template_file, 'rb') as f: xml_data = f.read() self.tree = ET.ElementTree(ET.fromstring(xml_data)) self.root = self.tree.getroot() return True except Exception as e: print(f"Error al cargar plantilla: {e}") return False def clean_project(self): """Limpiar el proyecto manteniendo solo la estructura base""" # Encontrar y limpiar clips existentes de forma segura arranger_automations = self.root.findall('.//ArrangerAutomation') for arranger in arranger_automations: # Limpiar eventos events = arranger.find('Events') if events is not None: events.clear() # Limpiar notas existentes for notes_container in self.root.findall('.//Notes'): # Solo limpiar si tiene contenido if len(notes_container) > 0: notes_container.clear() def rename_tracks(self, track_names: List[str]): """Renombrar tracks con los nombres especificados""" tracks = self.root.findall('.//MidiTrack') group_tracks = self.root.findall('.//GroupTrack') # Renombrar GroupTrack principal if group_tracks: name_elem = group_tracks[0].find('Name') if name_elem is not None: user_name = name_elem.find('UserName') effective_name = name_elem.find('EffectiveName') if user_name is not None: user_name.set('Value', track_names[0] if len(track_names) > 0 else 'Drums') if effective_name is not None: effective_name.set('Value', track_names[0] if len(track_names) > 0 else 'Drums') # Renombrar MidiTracks for i, track in enumerate(tracks): name_elem = track.find('Name') if name_elem is not None: user_name = name_elem.find('UserName') effective_name = name_elem.find('EffectiveName') if user_name is not None: user_name.set('Value', track_names[i] if i < len(track_names) else f'Track {i+1}') if effective_name is not None: effective_name.set('Value', track_names[i] if i < len(track_names) else f'Track {i+1}') def create_simple_clip_pattern(self, track_index: int, pattern_type: str = "Pattern 1"): """Crear un clip simple con patrón básico""" tracks = self.root.findall('.//MidiTrack') if track_index >= len(tracks): return track = tracks[track_index] # Buscar ArrangerAutomation arranger = track.find('.//ArrangerAutomation') if arranger is None: return # Crear MidiClip clip = ET.Element('MidiClip', { 'Id': '0', 'Time': '0' }) # LomId ET.SubElement(clip, 'LomId', Value='0') ET.SubElement(clip, 'LomIdView', Value='0') # Tiempo ET.SubElement(clip, 'CurrentStart', Value='0') ET.SubElement(clip, 'CurrentEnd', Value='4') # Loop loop = ET.SubElement(clip, 'Loop') ET.SubElement(loop, 'LoopStart', Value='0') ET.SubElement(loop, 'LoopEnd', Value='4') ET.SubElement(loop, 'StartRelative', Value='0') ET.SubElement(loop, 'LoopOn', Value='false') ET.SubElement(loop, 'OutMarker', Value='32') ET.SubElement(loop, 'HiddenLoopStart', Value='0') ET.SubElement(loop, 'HiddenLoopEnd', Value='32') # Nombre ET.SubElement(clip, 'Name', Value=pattern_type) ET.SubElement(clip, 'Annotation', Value='') ET.SubElement(clip, 'ColorIndex', Value='36') # Configuración básica ET.SubElement(clip, 'LaunchMode', Value='0') ET.SubElement(clip, 'LaunchQuantisation', Value='0') # TimeSignature time_sig = ET.SubElement(clip, 'TimeSignature') signatures = ET.SubElement(time_sig, 'TimeSignatures') remote_sig = ET.SubElement(signatures, 'RemoteableTimeSignature', Id='0') ET.SubElement(remote_sig, 'Numerator', Value='4') ET.SubElement(remote_sig, 'Denominator', Value='4') ET.SubElement(remote_sig, 'Time', Value='0') # Envelopes envelopes = ET.SubElement(clip, 'Envelopes') ET.SubElement(envelopes, 'Envelopes') # ScrollerTimePreserver scroller = ET.SubElement(clip, 'ScrollerTimePreserver') ET.SubElement(scroller, 'LeftTime', Value='0') ET.SubElement(scroller, 'RightTime', Value='32') # TimeSelection time_sel = ET.SubElement(clip, 'TimeSelection') ET.SubElement(time_sel, 'AnchorTime', Value='2') ET.SubElement(time_sel, 'OtherTime', Value='2') # Elementos vacíos ET.SubElement(clip, 'Legato') ET.SubElement(clip, 'Ram') # GrooveSettings groove = ET.SubElement(clip, 'GrooveSettings') ET.SubElement(groove, 'GrooveId', Value='0') # Configuración final ET.SubElement(clip, 'Disabled', Value='false') ET.SubElement(clip, 'VelocityAmount', Value='0') ET.SubElement(clip, 'FollowTime', Value='4') ET.SubElement(clip, 'FollowActionA', Value='0') ET.SubElement(clip, 'FollowActionB', Value='0') ET.SubElement(clip, 'FollowChanceA', Value='1') ET.SubElement(clip, 'FollowChanceB', Value='0') # Grid grid = ET.SubElement(clip, 'Grid') ET.SubElement(grid, 'FixedNumerator', Value='1') ET.SubElement(grid, 'FixedDenominator', Value='16') ET.SubElement(grid, 'GridIntervalPixel', Value='20') ET.SubElement(grid, 'Ntoles', Value='2') ET.SubElement(grid, 'SnapToGrid', Value='true') ET.SubElement(grid, 'Fixed', Value='false') ET.SubElement(clip, 'FreezeStart', Value='0') ET.SubElement(clip, 'FreezeEnd', Value='0') ET.SubElement(clip, 'IsWarped', Value='true') # Notas notes = ET.SubElement(clip, 'Notes') key_tracks = ET.SubElement(notes, 'KeyTracks') key_track = ET.SubElement(key_tracks, 'KeyTrack', Id='60') ET.SubElement(key_track, 'MidiKey', Value='60') notes_container = ET.SubElement(key_track, 'Notes') # Añadir 8 notas por defecto for i in range(8): ET.SubElement(notes_container, 'MidiNoteEvent', { 'Time': str(i), 'Duration': '0.5', 'Velocity': '100', 'OffVelocity': '64', 'IsEnabled': 'true' }) arranger.append(clip) def generate_project(self, output_name: str, track_names: List[str] = None): """Generar proyecto completo""" if not self.load_template(): return False if track_names is None: track_names = ['Drums', 'Kick', 'Snare', 'HiHat', 'Bass'] # Limpiar proyecto self.clean_project() # Renombrar tracks self.rename_tracks(track_names) # Crear clips simples en cada track for i in range(min(4, len(self.root.findall('.//MidiTrack')))): self.create_simple_clip_pattern(i, f"Pattern {i+1}") # Guardar self.save_als(output_name) return True def save_als(self, filename: str): """Guardar archivo .als""" try: self.output_file = filename tree = ET.ElementTree(self.root) # Escribir a archivo temporal temp_file = filename + '.tmp' tree.write(temp_file, encoding='utf-8', xml_declaration=True) # Leer y comprimir with open(temp_file, 'rb') as f: xml_data = f.read() with gzip.open(filename, 'wb') as f: f.write(xml_data) # Eliminar temporal os.remove(temp_file) print(f"Archivo .als generado: {filename}") return True except Exception as e: print(f"Error al guardar: {e}") return False def main(): """Función principal""" print("=" * 70) print("Generador de Archivos .als - Versión Corregida") print("=" * 70) # Verificar que existe el archivo original if not os.path.exists("jukeblocks - Pop.als"): print("❌ Error: No se encuentra 'jukeblocks - Pop.als'") print(" Este archivo es necesario como plantilla.") return # Generar proyecto generator = ALSGeneratorFixed() success = generator.generate_project( output_name="ren.als", track_names=['Drums', 'Kick', 'Snare', 'HiHat', 'Bass', 'Lead'] ) if success: print("\n✅ Archivo ren.als generado exitosamente") print(" Compatible con Ableton Live 12 Suite") else: print("\n❌ Error al generar el archivo") if __name__ == '__main__': main()