- 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
1212 lines
44 KiB
Python
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}")
|