Files
ableton-mcp-ai/mcp_server/engines/pattern_library.py
OpenCode Agent 5ce8187c65 feat: Implement senior audio injection with 5 fallback methods
- Add _cmd_create_arrangement_audio_pattern with 5-method fallback chain
- Method 1: track.insert_arrangement_clip() [Live 12+]
- Method 2: track.create_audio_clip() [Live 11+]
- Method 3: arrangement_clips.add_new_clip() [Live 12+]
- Method 4: Session->duplicate_clip_to_arrangement [Legacy]
- Method 5: Session->Recording [Universal]

- Add _cmd_duplicate_clip_to_arrangement for session-to-arrangement workflow
- Update skills documentation
- Verified: 3 clips created at positions [0, 4, 8] in Arrangement View

Closes: Audio injection in Arrangement View
2026-04-12 14:02:32 -03:00

1212 lines
44 KiB
Python

"""
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}")