""" pattern_library.py - Biblioteca de patrones musicales profesionales para reggaeton Contiene patrones de dembow, bajos, progresiones de acordes, generadores de melodías y utilidades para humanización. Timing en beats (float), reggaeton típicamente 4/4 @ 90-100 BPM """ import random from typing import List, Tuple, Optional, Dict, Any from dataclasses import dataclass from enum import Enum @dataclass class NoteEvent: """Representa un evento de nota MIDI""" pitch: int start_time: float # En beats duration: float # En beats velocity: int # 0-127 def copy(self) -> 'NoteEvent': return NoteEvent(self.pitch, self.start_time, self.duration, self.velocity) class ScaleType(Enum): MINOR = "minor" MAJOR = "major" PENTATONIC_MINOR = "pentatonic_minor" BLUES = "blues" class DembowPatterns: """ Patrones de dembow profesionales para reggaeton. El dembow es el ritmo característico del reggaeton. """ # Notas MIDI estándar para drums KICK_NOTE = 36 # C1 SNARE_NOTE = 38 # D1 HIHAT_CLOSED = 42 # F#1 HIHAT_OPEN = 46 # A#1 CLAP_NOTE = 39 # D#1 RIMSHOT_NOTE = 37 # C#1 # Tiempos de dembow en beats (cada beat = 1 cuarto nota) # Patrón clásico: kick en 1, snare en 2.25 y 4, etc. @staticmethod def get_kick_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]: """ Genera patrón de kick/bombo. Variaciones: - standard: Patrón dembow clásico - double: Doble tiempo en ciertos beats - triple: Patrón tresillo - minimal: Menos kicks, más espacio """ notes = [] beat_duration = 0.25 # 1/16 nota = 0.25 beats if variation == "standard": # Dembow clásico: kick en 1, 3, 4.25, 4.75 de cada compás for bar in range(bars): bar_offset = bar * 4.0 # Kick en tiempo 1 (beat 0 del compás) notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 120 )) # Kick en tiempo 3 (beat 2 del compás) notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 110 )) # Kick ghost en 4.25 (anticipación) notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + 3.25, 0.125, 80 )) # Kick en 4.75 (cierre) notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + 3.75, 0.125, 90 )) elif variation == "double": # Más kicks, doble tiempo en ciertos momentos for bar in range(bars): bar_offset = bar * 4.0 # Kick fuerte en 1 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 127)) # Kick en off-beat notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.75, 0.125, 100)) # Kick en 2.5 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 1.5, 0.25, 115)) # Kick en 3 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 120)) # Kick en off-beat 3 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.75, 0.125, 95)) # Dos kicks rápidos al final notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.25, 0.125, 90)) notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.125, 100)) notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.75, 0.125, 110)) elif variation == "triple": # Patrón tresillo más complejo tresillo_interval = 4.0 / 3.0 # Tresillo = 1.333 beats for bar in range(bars): bar_offset = bar * 4.0 for i in range(3): notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + (i * tresillo_interval), 0.3, 120 if i == 0 else 100 )) # Kick adicional en el último 16vo notes.append(NoteEvent( DembowPatterns.KICK_NOTE, bar_offset + 3.75, 0.125, 90 )) elif variation == "minimal": # Estilo minimal, menos es más for bar in range(bars): bar_offset = bar * 4.0 # Solo kick en 1 y 3 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 0.0, 0.25, 125)) if bar % 2 == 0: # Cada dos compases notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 2.0, 0.25, 110)) # Sub-bajo sutil en 4 notes.append(NoteEvent(DembowPatterns.KICK_NOTE, bar_offset + 3.5, 0.25, 85)) else: raise ValueError(f"Variación de kick no válida: {variation}") return notes @staticmethod def get_snare_pattern(bars: int = 16, variation: str = "standard") -> List[NoteEvent]: """ Genera patrón de snare/caja. El dembow clásico tiene snare en 2.25 (beat 2 + 1/4) y 4. """ notes = [] if variation == "standard": # Snare clásico dembow: tiempo 2.25 y 4 for bar in range(bars): bar_offset = bar * 4.0 # Snare principal en 2.25 (el característico) notes.append(NoteEvent( DembowPatterns.SNARE_NOTE, bar_offset + 1.25, # Beat 2 + 1/4 0.15, 115 )) # Snare en 4 notes.append(NoteEvent( DembowPatterns.SNARE_NOTE, bar_offset + 3.0, 0.2, 120 )) # Ghost note sutil en 2.75 if bar % 2 == 1: # Cada dos compases notes.append(NoteEvent( DembowPatterns.RIMSHOT_NOTE, bar_offset + 1.75, 0.1, 70 )) elif variation == "double": # Más snares, estilo más agresivo for bar in range(bars): bar_offset = bar * 4.0 notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.0, 0.15, 110)) notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 1.25, 0.15, 120)) notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.0, 0.2, 125)) # Roll en el último beat notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.5, 0.1, 100)) notes.append(NoteEvent(DembowPatterns.SNARE_NOTE, bar_offset + 3.75, 0.1, 90)) elif variation == "triple": # Patrón tresillo para snare tresillo_offsets = [1.0, 2.333, 3.666] for bar in range(bars): bar_offset = bar * 4.0 for i, offset in enumerate(tresillo_offsets): notes.append(NoteEvent( DembowPatterns.SNARE_NOTE, bar_offset + offset, 0.2, 115 )) elif variation == "minimal": # Snare minimalista for bar in range(bars): bar_offset = bar * 4.0 notes.append(NoteEvent( DembowPatterns.SNARE_NOTE, bar_offset + 1.25, 0.15, 110 )) # Solo en compases pares el segundo snare if bar % 2 == 0: notes.append(NoteEvent( DembowPatterns.SNARE_NOTE, bar_offset + 3.0, 0.2, 105 )) return notes @staticmethod def get_hihat_pattern(bars: int = 16, style: str = "8th", swing: float = 0.6) -> List[NoteEvent]: """ Genera patrón de hi-hats. Estilos: "8th", "16th", "32nd", "open", "pedal" Swing: 0.0-1.0, donde 0.5 es recto, >0.5 es swingado """ notes = [] # Factor de swing: cuánto se retrasa el off-beat swing_amount = (swing - 0.5) * 0.5 # Rango -0.25 a +0.25 if style == "8th": # Corcheas: en cada 1/2 beat for bar in range(bars): bar_offset = bar * 4.0 for eighth in range(8): beat_pos = bar_offset + (eighth * 0.5) # Aplicar swing a los off-beats (impares) if eighth % 2 == 1: beat_pos += swing_amount # Dinámica: acentos en 2 y 4 velocity = 100 if eighth in [2, 6]: # Tiempos 1.0 y 3.0 (beats 2 y 4) velocity = 115 elif eighth in [0, 4]: # Downbeats velocity = 110 else: velocity = 90 notes.append(NoteEvent( DembowPatterns.HIHAT_CLOSED, beat_pos, 0.1, velocity )) elif style == "16th": # Semicorcheas: más denso for bar in range(bars): bar_offset = bar * 4.0 for sixteenth in range(16): beat_pos = bar_offset + (sixteenth * 0.25) # Swing en off-beats if sixteenth % 2 == 1: beat_pos += swing_amount * 0.5 # Pattern de velocidades tipo "trap" if sixteenth % 4 == 0: # Cuartos velocity = 110 elif sixteenth % 2 == 0: # Octavas velocity = 95 else: # 16avos velocity = 85 notes.append(NoteEvent( DembowPatterns.HIHAT_CLOSED, beat_pos, 0.08, velocity )) elif style == "32nd": # Fusas: muy denso, estilo moderno for bar in range(bars): bar_offset = bar * 4.0 for i in range(32): beat_pos = bar_offset + (i * 0.125) # Roll de 32avos en el último beat if i >= 28: velocity = 100 + (i - 28) * 5 # Crescendo else: velocity = 80 if i % 2 == 1 else 70 notes.append(NoteEvent( DembowPatterns.HIHAT_CLOSED, beat_pos, 0.05, velocity )) elif style == "open": # Hi-hat abierto en ciertos tiempos open_times = [1.5, 3.5] # Off-beats de 2 y 4 for bar in range(bars): bar_offset = bar * 4.0 # Cerrados en corcheas for eighth in range(8): beat_pos = bar_offset + (eighth * 0.5) if eighth % 2 == 1: beat_pos += swing_amount # Verificar si es tiempo de abierto time_in_bar = eighth * 0.5 if any(abs(time_in_bar - ot) < 0.01 for ot in open_times): # Hi-hat abierto notes.append(NoteEvent( DembowPatterns.HIHAT_OPEN, beat_pos, 0.3, # Más largo 110 )) else: notes.append(NoteEvent( DembowPatterns.HIHAT_CLOSED, beat_pos, 0.1, 100 )) elif style == "pedal": # Estilo pedal - más sutil for bar in range(bars): bar_offset = bar * 4.0 # Solo en corcheas pares, suave for eighth in [0, 2, 4, 6]: beat_pos = bar_offset + (eighth * 0.5) notes.append(NoteEvent( DembowPatterns.HIHAT_CLOSED, beat_pos, 0.15, 75 )) return notes class BassPatterns: """ Patrones de bajo sub para reggaeton profesional. """ # Notas MIDI para bajo (C1 = 36, generalmente) @staticmethod def get_bass_line(bars: int = 16, progression: List[str] = None, key: str = "A", style: str = "sub") -> List[NoteEvent]: """ Genera línea de bajo. Progresión: lista de nombres de acordes (ej: ["Am", "F", "C", "G"]) Estilos: - sub: Sub-bajos largos y profundos - sustained: Notas sostenidas con release largo - pluck: Notas cortas y percusivas - slide: Con slides entre notas """ notes = [] if progression is None: # Progresión por defecto: vi-IV-I-V progression = ["Am", "F", "C", "G"] # Convertir acordes a notas raíz (MIDI) root_notes = BassPatterns._chords_to_roots(progression, key) # Duración por acorde beats_per_chord = 4.0 * bars / len(progression) if style == "sub": # Sub-bajos: notas largas en raíz for i, root in enumerate(root_notes): start = i * beats_per_chord duration = beats_per_chord * 0.9 # Dejar espacio al final # Octava baja para sub pitch = root - 12 # Una octava abajo notes.append(NoteEvent(pitch, start, duration, 110)) # Ghost note en quinta para rellenar if i % 2 == 0: fifth = pitch + 7 notes.append(NoteEvent(fifth, start + duration * 0.5, 0.25, 70)) elif style == "sustained": # Notas sostenidas con release for i, root in enumerate(root_notes): start = i * beats_per_chord duration = beats_per_chord # Llenar todo pitch = root - 12 # Velocidad con acento en el inicio notes.append(NoteEvent(pitch, start, duration, 120)) # Octava arriba para relleno armónico notes.append(NoteEvent(pitch + 12, start + 0.5, duration - 0.5, 90)) elif style == "pluck": # Notas cortas y percusivas for i, root in enumerate(root_notes): start = i * beats_per_chord # Dos notas por acorde pitch = root - 12 # Nota principal notes.append(NoteEvent(pitch, start, 0.25, 115)) # Octava arriba, staccato notes.append(NoteEvent(pitch + 12, start + 0.5, 0.15, 100)) # Off-beat adicional notes.append(NoteEvent(pitch, start + beats_per_chord * 0.75, 0.2, 90)) elif style == "slide": # Con slides/portamento entre notas for i, root in enumerate(root_notes): start = i * beats_per_chord pitch = root - 12 # Nota principal larga notes.append(NoteEvent(pitch, start, beats_per_chord * 0.8, 110)) # Slide a la siguiente nota if i < len(root_notes) - 1: next_pitch = root_notes[i + 1] - 12 slide_start = start + beats_per_chord * 0.8 slide_duration = beats_per_chord * 0.2 # Nota de slide (usamos nota de paso) if next_pitch > pitch: slide_note = pitch + 1 # Semitono arriba else: slide_note = pitch - 1 # Semitono abajo notes.append(NoteEvent(slide_note, slide_start, slide_duration, 80)) return notes @staticmethod def _chords_to_roots(progression: List[str], key: str) -> List[int]: """Convierte nombres de acordes a notas MIDI raíz""" # Notas base en octava 4 (C4 = 60) note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] # Encontrar offset del key if key in note_names: key_offset = note_names.index(key) else: key_offset = 9 # Default A # C4 = 60, así que A3 = 57 base_note = 57 + key_offset # A3 por defecto si key=A # Intervalos para acordes (relativos a la tonalidad) roman_intervals = { "I": 0, "i": 0, "II": 2, "ii": 2, "III": 4, "iii": 4, "IV": 5, "iv": 5, "V": 7, "v": 7, "VI": 9, "vi": 9, "VII": 11, "vii": 11, } roots = [] for chord in progression: # Extraer nota base del nombre del acorde if len(chord) >= 2 and chord[1] in ["#", "b"]: chord_root = chord[:2] quality = chord[2:] else: chord_root = chord[:1] quality = chord[1:] # Convertir a número de nota if chord_root in note_names: root_num = note_names.index(chord_root) elif chord_root.upper() in roman_intervals: root_num = (base_note % 12 + roman_intervals[chord_root.upper()]) % 12 else: root_num = base_note % 12 # Construir nota MIDI completa (octava 3) midi_note = 48 + root_num # C3 base if midi_note < base_note - 12: midi_note += 12 roots.append(midi_note) return roots class ChordProgressions: """ Progresiones de acordes estándar para reggaeton. """ # Progresiones predefinidas (notas como números romanos o nombres) PROGRESSIONS = { "vi-IV-I-V": ["Am", "F", "C", "G"], "i-VI-VII": ["Am", "F", "G"], "i-iv-VII-VI": ["Am", "Dm", "G", "F"], "i-VI-III-VII": ["Am", "F", "C", "G"], "ii-V-I": ["Dm", "G", "C"], "I-V-vi-IV": ["C", "G", "Am", "F"], "vi-V-IV-III": ["Am", "G", "F", "E"], "i-VII-VI-VII": ["Am", "G", "F", "G"], # Muy común en reggaeton } # Estructuras de acordes (triadas) CHORD_VOICINGS = { "major": [0, 4, 7], # 1, 3, 5 "minor": [0, 3, 7], # 1, b3, 5 "dim": [0, 3, 6], # 1, b3, b5 "aug": [0, 4, 8], # 1, 3, #5 "maj7": [0, 4, 7, 11], # 1, 3, 5, 7 "min7": [0, 3, 7, 10], # 1, b3, 5, b7 "dom7": [0, 4, 7, 10], # 1, 3, 5, b7 "sus4": [0, 5, 7], # 1, 4, 5 } @staticmethod def get_progression(name: str, key: str = "A", bars: int = 16) -> List[Dict[str, Any]]: """ Obtiene progresión de acordes con timing. Retorna lista de dicts con: chord_name, root_pitch, notes, start_beat, duration """ if name in ChordProgressions.PROGRESSIONS: chord_names = ChordProgressions.PROGRESSIONS[name] else: chord_names = name.split("-") # Convertir a notas result = [] beats_per_chord = 4.0 * bars / len(chord_names) note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] key_offset = note_names.index(key) if key in note_names else 9 # Default A base_note = 57 # A3 for i, chord_name in enumerate(chord_names): # Parsear nombre de acorde if len(chord_name) >= 2 and chord_name[1] in ["#", "b"]: root_name = chord_name[:2] quality = chord_name[2:] else: root_name = chord_name[:1] quality = chord_name[1:] # Encontrar nota raíz if root_name in note_names: root_num = note_names.index(root_name) else: root_num = key_offset # Ajustar a octava apropiada root_pitch = 48 + root_num # C3 base if root_pitch < base_note - 12: root_pitch += 12 # Determinar calidad if quality in ["m", "min", "minor", "-"]: voicing = "min7" elif quality in ["7", "dom"]: voicing = "dom7" elif quality in ["maj7", "M7"]: voicing = "maj7" elif quality == "sus4": voicing = "sus4" elif quality in ["dim", "°"]: voicing = "dim" else: voicing = "min7" if "m" in quality else "dom7" # Construir notas del acorde intervals = ChordProgressions.CHORD_VOICINGS.get(voicing, ChordProgressions.CHORD_VOICINGS["minor"]) chord_notes = [root_pitch + interval for interval in intervals] # Voicing en posición cercana (inversiones) chord_notes = ChordProgressions._optimize_voicing(chord_notes) result.append({ "chord_name": chord_name, "root_pitch": root_pitch, "notes": chord_notes, "start_beat": i * beats_per_chord, "duration": beats_per_chord, "voicing": voicing }) return result @staticmethod def _optimize_voicing(notes: List[int]) -> List[int]: """Optimiza voicing para que las notas estén cerca entre sí""" if len(notes) <= 1: return notes # Asegurar que todas las notas estén en un rango de una octava result = [notes[0]] for note in notes[1:]: # Encontrar octava más cercana while note - result[-1] > 6: note -= 12 while note - result[-1] < -6: note += 12 result.append(note) return sorted(result) @staticmethod def get_all_progression_names() -> List[str]: """Retorna todos los nombres de progresiones disponibles""" return list(ChordProgressions.PROGRESSIONS.keys()) class MelodyGenerator: """ Generador de melodías para reggaeton. """ # Escalas (intervalos semitonos) SCALES = { "minor": [0, 2, 3, 5, 7, 8, 10], # Natural minor "major": [0, 2, 4, 5, 7, 9, 11], # Major "pentatonic_minor": [0, 3, 5, 7, 10], # Pentatonic minor "pentatonic_major": [0, 2, 4, 7, 9], # Pentatonic major "blues": [0, 3, 5, 6, 7, 10], # Blues scale "dorian": [0, 2, 3, 5, 7, 9, 10], # Dorian mode "phrygian": [0, 1, 3, 5, 7, 8, 10], # Phrygian mode "harmonic_minor": [0, 2, 3, 5, 7, 8, 11], # Harmonic minor } @staticmethod def generate_melody(bars: int = 16, scale: str = "minor", density: float = 0.5, key: str = "A") -> List[NoteEvent]: """ Genera melodía automáticamente. density: 0.0-1.0, probabilidad de nota por subdivisión """ notes = [] # Obtener escala if scale in MelodyGenerator.SCALES: intervals = MelodyGenerator.SCALES[scale] else: intervals = MelodyGenerator.SCALES["minor"] # Encontrar nota raíz note_names = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] key_offset = note_names.index(key) if key in note_names else 9 root_pitch = 60 + key_offset # C4 base # Generar notas disponibles (2 octavas) available_notes = [] for octave in [0, 1]: # 2 octavas for interval in intervals: available_notes.append(root_pitch + interval + (octave * 12)) # Subdivisiones por compás según densidad if density < 0.3: subdivisions = 4 # Negras elif density < 0.6: subdivisions = 8 # Corcheas else: subdivisions = 16 # Semicorcheas subdivision_duration = 4.0 / subdivisions # Generar notas for bar in range(bars): bar_offset = bar * 4.0 for sub in range(subdivisions): if random.random() < density: start_time = bar_offset + (sub * subdivision_duration) # Seleccionar nota (preferir notas de acorde: 1, 3, 5) if random.random() < 0.7: # Nota de acorde (1, 3, 5) degree = random.choice([0, 2, 4]) # Índices en escala octave = random.choice([0, 1]) pitch = root_pitch + intervals[degree] + (octave * 12) else: # Cualquier nota de la escala pitch = random.choice(available_notes) # Duración según posición if sub % 4 == 0: # Tiempo fuerte duration = subdivision_duration * 2 velocity = 110 elif sub % 2 == 0: # Semi-fuerte duration = subdivision_duration * 1.5 velocity = 100 else: # Débil duration = subdivision_duration velocity = 90 notes.append(NoteEvent(pitch, start_time, duration, velocity)) # Ordenar por tiempo notes.sort(key=lambda n: n.start_time) # Asegurar que no haya superposiciones excesivas notes = MelodyGenerator._clean_overlaps(notes) return notes @staticmethod def _clean_overlaps(notes: List[NoteEvent]) -> List[NoteEvent]: """Limpia superposiciones de notas en el mismo pitch""" if not notes: return notes # Agrupar por pitch by_pitch = {} for note in notes: if note.pitch not in by_pitch: by_pitch[note.pitch] = [] by_pitch[note.pitch].append(note) # Limpiar cada grupo cleaned = [] for pitch, pitch_notes in by_pitch.items(): pitch_notes.sort(key=lambda n: n.start_time) for i, note in enumerate(pitch_notes): if i > 0: prev = pitch_notes[i - 1] # Si se superpone, acortar la anterior if prev.start_time + prev.duration > note.start_time: prev.duration = note.start_time - prev.start_time cleaned.extend(pitch_notes) # Re-ordenar cleaned.sort(key=lambda n: n.start_time) return cleaned @staticmethod def generate_counter_melody(main_melody: List[NoteEvent], scale: str = "minor", interval: int = 3) -> List[NoteEvent]: """ Genera contramelodía a partir de melodía principal. interval: intervalo de contrapunto (3 = tercera, 6 = sexta) """ counter_notes = [] for note in main_melody: # Añadir nota a intervalo especificado counter_pitch = note.pitch + interval # Ajustar a escala si es necesario intervals = MelodyGenerator.SCALES.get(scale, MelodyGenerator.SCALES["minor"]) root = note.pitch % 12 target = counter_pitch % 12 # Verificar si está en escala scale_notes = [(root + i) % 12 for i in intervals] if target not in scale_notes: # Ajustar al grado más cercano counter_pitch += 1 if random.random() > 0.5 else -1 # Más corta y suave que la original counter_notes.append(NoteEvent( counter_pitch, note.start_time + 0.0625, # Ligeramente después note.duration * 0.7, int(note.velocity * 0.75) )) return counter_notes class HumanFeel: """ Aplica humanización a patrones MIDI para hacerlos más naturales. """ @staticmethod def apply_micro_timing(notes: List[NoteEvent], variance_ms: float = 15) -> List[NoteEvent]: """ Ajusta timing de notas ±variance_ms milisegundos. Asume BPM promedio de 95 para convertir ms a beats. """ bpm = 95.0 ms_per_beat = 60000.0 / bpm # ms por beat variance_beats = variance_ms / ms_per_beat result = [] for note in notes: new_note = note.copy() # Variación aleatoria gaussiana offset = random.gauss(0, variance_beats) new_note.start_time += offset # Asegurar que no sea negativo new_note.start_time = max(0, new_note.start_time) result.append(new_note) return result @staticmethod def apply_velocity_variation(notes: List[NoteEvent], variance: int = 10) -> List[NoteEvent]: """ Aplica variación de velocidad ±variance. """ result = [] for note in notes: new_note = note.copy() # Variación aleatoria vel_change = random.randint(-variance, variance) new_note.velocity = max(1, min(127, note.velocity + vel_change)) result.append(new_note) return result @staticmethod def apply_length_variation(notes: List[NoteEvent], variance_percent: float = 5.0) -> List[NoteEvent]: """ Aplica variación de duración ±variance_percent%. """ result = [] variance_decimal = variance_percent / 100.0 for note in notes: new_note = note.copy() # Variación porcentual factor = 1.0 + random.uniform(-variance_decimal, variance_decimal) new_note.duration = max(0.01, note.duration * factor) result.append(new_note) return result @staticmethod def apply_all_humanization(notes: List[NoteEvent], timing_variance_ms: float = 15, velocity_variance: int = 10, length_variance_percent: float = 5.0) -> List[NoteEvent]: """ Aplica todas las humanizaciones en secuencia. """ result = HumanFeel.apply_micro_timing(notes, timing_variance_ms) result = HumanFeel.apply_velocity_variation(result, velocity_variance) result = HumanFeel.apply_length_variation(result, length_variance_percent) return result @staticmethod def apply_timing_bias(notes: List[NoteEvent], bias: str = "lay_back") -> List[NoteEvent]: """ Aplica sesgo de timing al compás. bias: "lay_back" (detrás del beat), "ahead" (adelante), "center" (centro) """ bpm = 95.0 ms_per_beat = 60000.0 / bpm if bias == "lay_back": # Detrás del beat: +10-20ms offset_ms = random.uniform(10, 20) elif bias == "ahead": # Adelante del beat: -10-20ms offset_ms = random.uniform(-20, -10) else: return [n.copy() for n in notes] offset_beats = offset_ms / ms_per_beat result = [] for note in notes: new_note = note.copy() new_note.start_time += offset_beats new_note.start_time = max(0, new_note.start_time) result.append(new_note) return result class PercussionLibrary: """ Librería de percusiones adicionales y efectos para reggaeton. """ # Notas MIDI para percusión PERCUSSION_NOTES = { "timbal": 47, # High floor tom "conga_low": 48, # High tom "conga_mid": 50, # High tom 2 "conga_high": 45, # Low tom "bongo_low": 60, # High bongo "bongo_high": 61, # Low bongo "claves": 75, # Claves "guiro": 73, # Short guiro "guiro_long": 74, # Long guiro "maracas": 70, # Maracas "cabasa": 69, # Cabasa "tambourine": 54, # Tambourine "agogo": 67, # High agogo "whistle": 72, # Whistle "triangle": 80, # Triangle "shaker": 82, # Shaker "timbale": 65, # High timbale "timbale_low": 66, # Low timbale } FX_NOTES = { "riser": 93, # Efecto de subida "downer": 91, # Efecto de bajada "sweep": 92, # Sweep "impact": 94, # Impacto "crash": 49, # Crash cymbal "reverse_crash": 55,# Reverse cymbal "fx_hit": 95, # Hit FX "noise": 96, # Noise burst "sub_drop": 97, # Sub drop "tape_stop": 98, # Tape stop effect } @staticmethod def get_percussion_fill(bars: int = 4, intensity: float = 0.7) -> List[NoteEvent]: """ Genera fill de percusión latina. intensity: 0.0-1.0, densidad del fill """ notes = [] # Instrumentos a usar según intensidad instruments = ["conga_mid", "conga_high", "timbale"] if intensity > 0.5: instruments.extend(["timbal", "bongo_high"]) if intensity > 0.7: instruments.append("claves") # Patrón de fills típico de reggaeton fill_patterns = [ # Patrón 1: Roll descendente [(0, "conga_high"), (0.25, "conga_mid"), (0.5, "conga_low"), (0.75, "timbale")], # Patrón 2: Alternado [(0, "conga_mid"), (0.125, "timbale"), (0.25, "conga_mid"), (0.375, "timbale"), (0.5, "conga_high"), (0.75, "conga_mid")], # Patrón 3: Tumbao [(0, "conga_low"), (0.5, "conga_mid"), (0.75, "conga_high"), (0.875, "conga_mid")], ] pattern = random.choice(fill_patterns) # Generar notas del fill for bar_offset_mul in range(bars): bar_offset = bar_offset_mul * 4.0 for time_offset, instrument in pattern: start = bar_offset + time_offset pitch = PercussionLibrary.PERCUSSION_NOTES.get(instrument, 60) # Velocidad según intensidad base_vel = 80 + int(intensity * 40) velocity = min(127, base_vel + random.randint(-10, 10)) notes.append(NoteEvent(pitch, start, 0.15, velocity)) return notes @staticmethod def get_fx_hit(position: float, fx_type: str = "riser", duration: float = 2.0) -> NoteEvent: """ Genera un efecto FX en posición específica. position: tiempo en beats fx_type: "riser", "downer", "impact", "crash", "sweep" duration: duración del FX en beats """ pitch = PercussionLibrary.FX_NOTES.get(fx_type, 93) velocity = 110 if fx_type in ["impact", "crash"] else 100 return NoteEvent(pitch, position, duration, velocity) @staticmethod def get_intro_buildup(bars: int = 4) -> List[NoteEvent]: """ Genera buildup para intro (subida de tensión). """ notes = [] # Cada vez más denso for bar in range(bars): bar_offset = bar * 4.0 density = (bar + 1) / bars # 0.25, 0.5, 0.75, 1.0 # Shaker cada vez más rápido subdivisions = int(4 + (density * 12)) # 4 a 16 for i in range(subdivisions): start = bar_offset + (i * (4.0 / subdivisions)) vel = 60 + int(density * 60) # Crescendo notes.append(NoteEvent( PercussionLibrary.PERCUSSION_NOTES["shaker"], start, 0.05, min(127, vel) )) # Riser final notes.append(PercussionLibrary.get_fx_hit(bars * 4.0 - 2.0, "riser", 2.0)) return notes @staticmethod def get_transition_fill(position: float, type: str = "break") -> List[NoteEvent]: """ Genera fill de transición. type: "break", "build", "drop", "impact" """ notes = [] if type == "break": # Silencio seguido de impacto notes.append(PercussionLibrary.get_fx_hit(position + 0.5, "reverse_crash", 1.0)) notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "impact", 0.5)) elif type == "build": # Build con congas for i in range(8): start = position + (i * 0.125) notes.append(NoteEvent( PercussionLibrary.PERCUSSION_NOTES["conga_mid"], start, 0.1, 80 + i * 5 )) notes.append(PercussionLibrary.get_fx_hit(position + 1.0, "sweep", 0.5)) elif type == "drop": # Drop con sub notes.append(PercussionLibrary.get_fx_hit(position, "sub_drop", 1.0)) notes.append(PercussionLibrary.get_fx_hit(position, "crash", 1.0)) elif type == "impact": # Impacto fuerte notes.append(PercussionLibrary.get_fx_hit(position, "impact", 0.8)) notes.append(NoteEvent( PercussionLibrary.FX_NOTES["crash"], position, 1.0, 127 )) return notes # Funciones de conveniencia def create_drum_pattern(style: str = "dembow", bars: int = 16, humanize: bool = True) -> Dict[str, List[NoteEvent]]: """ Crea patrón completo de batería. Retorna dict con: kick, snare, hihat """ dembow = DembowPatterns() kicks = dembow.get_kick_pattern(bars, variation=style if style in ["standard", "double", "triple", "minimal"] else "standard") snares = dembow.get_snare_pattern(bars, variation="standard") hihats = dembow.get_hihat_pattern(bars, style="16th", swing=0.6) if humanize: humanizer = HumanFeel() kicks = humanizer.apply_all_humanization(kicks, 10, 8, 3) snares = humanizer.apply_all_humanization(snares, 15, 10, 5) hihats = humanizer.apply_all_humanization(hihats, 5, 5, 2) return { "kick": kicks, "snare": snares, "hihat": hihats } def create_full_arrangement(bars_per_section: int = 16, key: str = "A") -> Dict[str, Any]: """ Crea arreglo completo de reggaeton. Retorna estructura con: intro, verse, chorus, bridge, outro """ arrangement = {} # Progresión prog = ChordProgressions.get_progression("vi-IV-I-V", key, bars_per_section) # Intro arrangement["intro"] = { "drums": create_drum_pattern("minimal", bars_per_section, True), "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F"], key, "sustained"), "chords": prog, "percussion": PercussionLibrary.get_intro_buildup(4) } # Verso arrangement["verse"] = { "drums": create_drum_pattern("standard", bars_per_section, True), "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "sub"), "chords": prog, "melody": MelodyGenerator.generate_melody(bars_per_section, "pentatonic_minor", 0.4, key) } # Coro arrangement["chorus"] = { "drums": create_drum_pattern("double", bars_per_section, True), "bass": BassPatterns.get_bass_line(bars_per_section, ["Am", "F", "C", "G"], key, "pluck"), "chords": prog, "melody": MelodyGenerator.generate_melody(bars_per_section, "minor", 0.6, key) } return arrangement # Constantes útiles NOTE_NAMES = ["C", "C#", "D", "D#", "E", "F", "F#", "G", "G#", "A", "A#", "B"] DRUM_NOTES = { "kick": 36, "snare": 38, "clap": 39, "rim": 37, "hihat_closed": 42, "hihat_open": 46, "hihat_pedal": 44, "crash": 49, "ride": 51, "tom1": 50, "tom2": 47, "tom3": 43, } def notes_to_dict_list(notes: List[NoteEvent]) -> List[Dict[str, Any]]: """Convierte lista de NoteEvent a lista de diccionarios""" return [ { "pitch": n.pitch, "start_time": n.start_time, "duration": n.duration, "velocity": n.velocity } for n in notes ] def dict_list_to_notes(dict_list: List[Dict[str, Any]]) -> List[NoteEvent]: """Convierte lista de diccionarios a lista de NoteEvent""" return [ NoteEvent( d["pitch"], d["start_time"], d["duration"], d["velocity"] ) for d in dict_list ] def get_patterns(pattern_type: str, **kwargs) -> Any: """ Función conveniencia para obtener patrones musicales. Args: pattern_type: Tipo de patrón ('drum', 'bass', 'chords', 'melody', 'percussion', 'arrangement') **kwargs: Argumentos específicos para cada tipo de patrón Returns: Patrón solicitado del tipo especificado Examples: >>> get_patterns('drum', style='dembow', bars=16) >>> get_patterns('bass', progression=['Am', 'F', 'C', 'G'], key='A', style='sub') >>> get_patterns('chords', progression_type='vi-IV-I-V', key='A', bars=16) """ if pattern_type == "drum": return create_drum_pattern(**kwargs) elif pattern_type == "bass": return BassPatterns.get_bass_line(**kwargs) elif pattern_type == "chords": return ChordProgressions.get_progression(**kwargs) elif pattern_type == "melody": return MelodyGenerator.generate_melody(**kwargs) elif pattern_type == "percussion": return PercussionLibrary.get_layered_percussion(**kwargs) elif pattern_type == "arrangement": return create_full_arrangement(**kwargs) else: raise ValueError(f"Tipo de patrón no soportado: {pattern_type}")