Cambios realizados:
1. TODO-008 - ROLE_SECTION_VARIANTS para reggaeton:
- Agregado REGGAETON_SECTION_VARIANTS con variantes especificas por rol y seccion:
* bass: intro=smooth deep, build=rising, drop=full punchy dembow, break=minimal rolling
* perc: intro=minimal, drop=full dembow latin percussion, break=sparse congas bongos
* vocal: intro=absent, build=tease, drop=full chop, break=phrase
* synth: intro=filtered, build=rising, drop=pluck hooky, break=pad atmospheric
- Modificado _get_section_variation() para aceptar parametro 'genre'
- Modificado _should_vary_role_in_section() para soportar reggaeton
2. TODO-011 - Linea de bajo dembow caracteristica:
- Agregado estilo 'bouncy' en create_bassline() con pattern bump-silencio-apoyo
- Agregado estilo 'dembow' en create_bassline() con linea que sigue el patron del kick
- Incluye slides/portamento entre notas para efecto caracteristico
3. TODO-014 - ARRANGEMENT_PROFILES para reggaeton:
- Agregado perfil 'dembow' para reggaeton:
* drum_tightness=0.92, bass_motion=bouncy, melodic_motion=hooky
* Bus names: DRUM DEMBOW, BASS TUBE, SYNTH PLUCK, VOCAL CHOP
- Agregado perfil 'moombahton' para reggaeton:
* drum_tightness=0.88, bass_motion=heavy, melodic_motion=anthemic
* BPM 105-112, mas heavy bass, influencia dancehall
4. T011/T012/T018 - Ya estaban implementados:
- _find_library_file() usa limit=50 (T011)
- Shuffle del pool con seed de sesion (T012)
- Palette Lock activado por defecto en generacion (T018)
TODOs completados:
- TODO-008: ROLE_SECTION_VARIANTS reggaeton
- TODO-011: Linea de bajo dembow
- TODO-014: ARRANGEMENT_PROFILES reggaeton
- TODO-015: Estilo moombahton
Refs: Fase 3 implementacion reggaeton completa
6331 lines
273 KiB
Python
6331 lines
273 KiB
Python
"""
|
||
song_generator.py - Generador musical para AbletonMCP-AI.
|
||
"""
|
||
|
||
import random
|
||
import logging
|
||
from typing import List, Dict, Any, Optional, Union, Tuple
|
||
from dataclasses import dataclass
|
||
from pathlib import Path
|
||
from collections import defaultdict
|
||
|
||
logger = logging.getLogger("SongGenerator")
|
||
|
||
# Notas MIDI para referencia
|
||
NOTE_NAMES = ['C', 'C#', 'D', 'D#', 'E', 'F', 'F#', 'G', 'G#', 'A', 'A#', 'B']
|
||
|
||
# Escalas comunes (semitonos desde la raÃz)
|
||
SCALES = {
|
||
'major': [0, 2, 4, 5, 7, 9, 11],
|
||
'minor': [0, 2, 3, 5, 7, 8, 10],
|
||
'harmonic_minor': [0, 2, 3, 5, 7, 8, 11],
|
||
'dorian': [0, 2, 3, 5, 7, 9, 10],
|
||
'phrygian': [0, 1, 3, 5, 7, 8, 10],
|
||
'mixolydian': [0, 2, 4, 5, 7, 9, 10],
|
||
'pentatonic_minor': [0, 3, 5, 7, 10],
|
||
'pentatonic_major': [0, 2, 4, 7, 9],
|
||
'blues': [0, 3, 5, 6, 7, 10],
|
||
}
|
||
|
||
# Progresiones de acordes comunes
|
||
CHORD_PROGRESSIONS = {
|
||
'techno': [
|
||
[1, 1, 1, 1], # i - i - i - i (minimal)
|
||
[1, 6, 1, 6], # i - VI - i - VI
|
||
[1, 4, 1, 4], # i - iv - i - iv
|
||
[1, 7, 6, 7], # i - VII - VI - VII
|
||
],
|
||
'house': [
|
||
[1, 5, 6, 4], # I - V - vi - IV (pop house)
|
||
[1, 4, 5, 1], # I - IV - V - I
|
||
[6, 4, 1, 5], # vi - IV - I - V
|
||
[1, 6, 4, 5], # I - vi - IV - V
|
||
],
|
||
'deep': [
|
||
[1, 6, 2, 5], # i - VI - ii - V
|
||
[2, 5, 1, 6], # ii - V - i - VI
|
||
],
|
||
'trance': [
|
||
[1, 5, 6, 4], # I - V - vi - IV
|
||
[6, 4, 1, 5], # vi - IV - I - V
|
||
[1, 4, 6, 5], # I - IV - vi - V
|
||
],
|
||
'reggaeton': [
|
||
[6, 4, 1, 5], # vi-IV-I-V (la mas usada en reggaeton)
|
||
[1, 5, 6, 4], # I-V-vi-IV (pop reggaeton)
|
||
[6, 3, 4, 1], # vi-III-IV-I (mas oscura, trap)
|
||
[1, 1, 4, 5], # I-I-IV-V (reggaeton romantico)
|
||
],
|
||
}
|
||
|
||
# Configuraciones por género
|
||
GENRE_CONFIGS = {
|
||
'techno': {
|
||
'bpm_range': (125, 140),
|
||
'default_bpm': 132,
|
||
'keys': ['Am', 'Fm', 'Dm', 'G#m', 'Cm'],
|
||
'styles': ['industrial', 'peak-time', 'dub', 'minimal', 'acid'],
|
||
},
|
||
'house': {
|
||
'bpm_range': (120, 128),
|
||
'default_bpm': 124,
|
||
'keys': ['Am', 'Em', 'Cm', 'Gm', 'Dm', 'F#m'],
|
||
'styles': ['deep', 'tech-house', 'progressive', 'afro', 'classic', 'funky'],
|
||
},
|
||
'tech-house': {
|
||
'bpm_range': (122, 128),
|
||
'default_bpm': 125,
|
||
'keys': ['Am', 'Fm', 'Dm', 'Gm', 'Cm'],
|
||
'styles': ['groovy', 'bouncy', 'minimal', 'latin', 'latin-industrial'],
|
||
},
|
||
'trance': {
|
||
'bpm_range': (135, 150),
|
||
'default_bpm': 140,
|
||
'keys': ['Fm', 'Am', 'Dm', 'Gm', 'Cm'],
|
||
'styles': ['progressive', 'uplifting', 'psy', 'acid'],
|
||
},
|
||
'drum-and-bass': {
|
||
'bpm_range': (160, 180),
|
||
'default_bpm': 174,
|
||
'keys': ['Am', 'Fm', 'Gm', 'Cm'],
|
||
'styles': ['liquid', 'neuro', 'jump-up', 'jungle'],
|
||
},
|
||
'reggaeton': {
|
||
'bpm_range': (90, 100),
|
||
'default_bpm': 95,
|
||
'keys': ['Am', 'Dm', 'Gm', 'Cm', 'Fm', 'Em'],
|
||
'styles': ['dembow', 'perreo', 'moombahton', 'latin-trap', 'romantico'],
|
||
},
|
||
}
|
||
|
||
# Colores por tipo de track
|
||
TRACK_COLORS = {
|
||
'kick': 10, # Rojo
|
||
'snare': 20, # Verde
|
||
'hat': 5, # Amarillo
|
||
'clap': 45, # Naranja
|
||
'bass': 30, # Azul
|
||
'synth': 50, # Rosa/Magenta
|
||
'chords': 60, # Púrpura
|
||
'fx': 25, # Verde claro
|
||
'vocal': 15, # Naranja oscuro
|
||
'pad': 55, # Purpura claro
|
||
'perc': 20, # Verde
|
||
'ride': 14, # Amarillo oscuro
|
||
'technical': 58, # Gris
|
||
}
|
||
|
||
BUS_TRACK_COLORS = {
|
||
'drums': 10,
|
||
'bass': 30,
|
||
'music': 50,
|
||
'vocal': 15,
|
||
'fx': 25,
|
||
'sc_trigger': 58, # Gris - track fantasma para sidechain
|
||
}
|
||
|
||
# Configuracion de sidechain por bus
|
||
# Cada bus puede tener sidechain desde SC TRIGGER
|
||
BUS_SIDECHAIN_CONFIG = {
|
||
'drums': {
|
||
'enabled': False, # Drums no suele necesitar sidechain
|
||
'threshold': -18.0,
|
||
'attack': 0.003,
|
||
'release': 0.08,
|
||
'ratio': 4.0,
|
||
},
|
||
'bass': {
|
||
'enabled': True, # Sidechain clave para bass
|
||
'threshold': -22.0,
|
||
'attack': 0.002,
|
||
'release': 0.12,
|
||
'ratio': 4.5,
|
||
},
|
||
'music': {
|
||
'enabled': True, # Sidechain sutil para musica
|
||
'threshold': -26.0,
|
||
'attack': 0.005,
|
||
'release': 0.18,
|
||
'ratio': 3.0,
|
||
},
|
||
'vocal': {
|
||
'enabled': True, # Sidechain suave para vocal
|
||
'threshold': -28.0,
|
||
'attack': 0.008,
|
||
'release': 0.22,
|
||
'ratio': 2.5,
|
||
},
|
||
'fx': {
|
||
'enabled': False, # FX generalmente sin sidechain
|
||
'threshold': -30.0,
|
||
'attack': 0.01,
|
||
'release': 0.3,
|
||
'ratio': 2.0,
|
||
},
|
||
}
|
||
|
||
# =============================================================================
|
||
# FASE 3: LOUDNESS CONSISTENCY Y GAIN STAGING
|
||
# =============================================================================
|
||
#
|
||
# CALIBRATION PHILOSOPHY:
|
||
# ======================
|
||
# - Kick sits at unity (0.85) as the rhythmic anchor
|
||
# - Bass sits slightly below kick (-1dB) for low-end presence without mud
|
||
# - Supporting elements progressively lower to create mix depth
|
||
# - Buses attenuated to preserve master headroom
|
||
# - Master chain with soft limiting for consistent output
|
||
#
|
||
# HEADROOM TARGETS:
|
||
# =================
|
||
# - Track peaks: -6dB to -3dB before bus
|
||
# - Bus peaks: -3dB to -1dB before master
|
||
# - Master out: -1dB peak (limited), integrated LUFS ~-10 to -8
|
||
|
||
# Headroom target en dB (negativo para dejar espacio antes del limiter)
|
||
TARGET_HEADROOM_DB = -1.5 # 1.5dB de headroom antes del limiter
|
||
|
||
# Safe limiting threshold - prevents digital clipping
|
||
MASTER_LIMITER_CEILING_DB = -0.3 # Never go above -0.3dBFS on master
|
||
|
||
# Calibracion de ganancia por bus (valores lineales 0.0-1.0)
|
||
# Calibrado empiricamente para headroom consistente y balance de mezcla
|
||
# K: Drums como elemento principal, B: Bass como soporte, M: Music como capa
|
||
BUS_GAIN_CALIBRATION = {
|
||
'drums': {
|
||
'volume': 0.92, # Drums bus: principal, mas alto
|
||
'limiter_gain': 0.0, # Sin gain adicional en limiter de bus
|
||
'compressor_threshold': -16.0, # Compression suave para punch
|
||
'saturator_drive': 0.6, # armonia sutil, no crunchy
|
||
'utility_gain': 0.0, # Sin gain adicional
|
||
},
|
||
'bass': {
|
||
'volume': 0.88, # Bass bus: soporte fuerte
|
||
'limiter_gain': 0.0, # Sin limiter en bass bus (soft clip natural)
|
||
'compressor_threshold': -18.0, # Threshold suave para low-end
|
||
'saturator_drive': 0.4, # Saturacion sutil - evitar crunch
|
||
'utility_gain': 0.0, # Sin gain adicional
|
||
},
|
||
'music': {
|
||
'volume': 0.85, # Music bus: capa principal
|
||
'limiter_gain': 0.0, # Sin limiter en music bus
|
||
'compressor_threshold': -20.0, # Preservar transients
|
||
'saturator_drive': 0.0, # Sin saturacion en bus de musica
|
||
'utility_gain': 0.0,
|
||
},
|
||
'vocal': {
|
||
'volume': 0.82, # Vocal bus: presente en mezcla
|
||
'limiter_gain': 0.0, # Sin limiter
|
||
'compressor_threshold': -16.0, # Compresion sutil para presencia
|
||
'saturator_drive': 0.0,
|
||
'utility_gain': 0.0,
|
||
},
|
||
'fx': {
|
||
'volume': 0.78, # FX bus: efectos audibles
|
||
'limiter_gain': 0.0, # Sin gain
|
||
'compressor_threshold': -22.0, # Preservar dynamics
|
||
'saturator_drive': 0.0,
|
||
'utility_gain': 0.0, # Sin reduccion
|
||
},
|
||
'sc_trigger': {
|
||
'volume': 0.0, # Track fantasma - sin audio
|
||
'limiter_gain': 0.0,
|
||
'compressor_threshold': 0.0,
|
||
'saturator_drive': 0.0,
|
||
'utility_gain': 0.0,
|
||
},
|
||
}
|
||
|
||
# Master chain calibracion
|
||
# Calibrado para LUFS ~-8 a -10dB con headroom de 1-2dB antes del limiter
|
||
# El limiter ceiling esta en -0.3dB para evitar digital clipping
|
||
MASTER_CALIBRATION = {
|
||
'default': {
|
||
'volume': 0.85, # Master at ~0dB de ganancia interna
|
||
'utility_gain': 0.0, # Sin reduccion - volumen completo
|
||
'stereo_width': 1.04, # Ligerisimo widening
|
||
'saturator_drive': 0.12, # Saturacion muy sutil en master
|
||
'compressor_ratio': 0.50, # Compresion suave (glue, no squash)
|
||
'compressor_attack': 0.30, # Attack lento para preservar transients
|
||
'compressor_release': 0.20,
|
||
'limiter_gain': 3.5, # +3.5dB make-up gain para nivel moderno
|
||
'limiter_ceiling': -0.3, # Ceiling a -0.3dBFS (safe limiting)
|
||
},
|
||
'warehouse': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0, # Sin reduccion
|
||
'saturator_drive': 0.25, # Mas drive para industrial techno
|
||
'compressor_ratio': 0.55, # Un poco mas de compresion
|
||
'limiter_gain': 3.8, # Mas gain para industrial
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'festival': {
|
||
'volume': 0.86,
|
||
'utility_gain': 0.0, # Sin reduccion
|
||
'stereo_width': 1.06, # Mas ancho para festival
|
||
'limiter_gain': 4.0, # Maximo gain para festival
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'swing': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0,
|
||
'saturator_drive': 0.15, # Moderado
|
||
'limiter_gain': 3.2,
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'jackin': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0,
|
||
'compressor_ratio': 0.52,
|
||
'limiter_gain': 3.0,
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'tech-house-club': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0, # Sin reduccion
|
||
'stereo_width': 1.04,
|
||
'saturator_drive': 0.4, # Mas drive para punch
|
||
'compressor_ratio': 0.60, # Mas compresion para club
|
||
'compressor_attack': 0.28,
|
||
'limiter_gain': 3.5,
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'tech-house-deep': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0, # Sin reduccion
|
||
'stereo_width': 1.02, # Narrower para deep
|
||
'saturator_drive': 0.1, # Muy sutil
|
||
'compressor_ratio': 0.50,
|
||
'compressor_attack': 0.38, # Mas lento para deep
|
||
'limiter_gain': 3.0,
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
'tech-house-funky': {
|
||
'volume': 0.85,
|
||
'utility_gain': 0.0,
|
||
'stereo_width': 1.08, # Wide para groove
|
||
'saturator_drive': 0.3,
|
||
'compressor_ratio': 0.55,
|
||
'compressor_attack': 0.30,
|
||
'limiter_gain': 3.5,
|
||
'limiter_ceiling': -0.3,
|
||
},
|
||
}
|
||
|
||
# Calibracion de gain por rol para consistencia de mezcla
|
||
# Valores calibrados empiricamente basados en:
|
||
# - Kick como ancla a 0.85
|
||
# - Bass -1dB relativo a kick
|
||
# - Elementos de soporte progresivamente mas bajos
|
||
# - Headroom preservado en cada capa
|
||
ROLE_GAIN_CALIBRATION = {
|
||
# DRUMS - Kick es el ancla, otros elementos debajo
|
||
'kick': {
|
||
'volume': 0.85, # Ancla: 0dB relativo, elemento principal
|
||
'saturator_drive': 1.5, # Saturacion sutil para punch
|
||
'peak_reduction': 0.0, # Sin reduccion - es el ancla
|
||
},
|
||
'clap': {
|
||
'volume': 0.78, # -1.5dB relativo a kick
|
||
'saturator_drive': 0.0, # Sin saturacion
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'snare_fill': {
|
||
'volume': 0.72, # -3dB, transitorio fuerte
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'hat_closed': {
|
||
'volume': 0.68, # -4dB, elemento secundario
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'hat_open': {
|
||
'volume': 0.65, # -4.5dB, mas abajo por sustain
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'top_loop': {
|
||
'volume': 0.62, # -5dB, capa ritmica secundaria
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'perc': {
|
||
'volume': 0.70, # -3.5dB, soporte ritmico
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'ride': {
|
||
'volume': 0.58, # -5.5dB, sustain largo
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'crash': {
|
||
'volume': 0.50, # -7dB, transitorio largo
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'tom_fill': {
|
||
'volume': 0.68, # -4dB, transitorio
|
||
'peak_reduction': 0.0,
|
||
},
|
||
# BASS - Underground but underneath drums
|
||
'sub_bass': {
|
||
'volume': 0.80, # -0.5dB relativo a kick
|
||
'saturator_drive': 0.0, # Sin saturacion en sub
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'bass': {
|
||
'volume': 0.78, # -1dB relativo a kick
|
||
'saturator_drive': 2.0, # Moderado para harmonic content
|
||
'peak_reduction': 0.0,
|
||
},
|
||
# MUSIC - Capas de soporte, debajo del low-end
|
||
'drone': {
|
||
'volume': 0.55, # -7dB, elemento de fondo
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'chords': {
|
||
'volume': 0.70, # -3dB, armonia principal
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'stab': {
|
||
'volume': 0.65, # -4dB, transitorio
|
||
'saturator_drive': 1.8, # Moderado
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'pad': {
|
||
'volume': 0.60, # -5dB, fondo armonico
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'pluck': {
|
||
'volume': 0.68, # -3.5dB, melodia sutil
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'arp': {
|
||
'volume': 0.65, # -4dB, movimiento armonico
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'lead': {
|
||
'volume': 0.72, # -2.5dB, elemento principal musical
|
||
'saturator_drive': 1.2, # Moderado
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'counter': {
|
||
'volume': 0.62, # -5dB, contramelodia
|
||
'peak_reduction': 0.0,
|
||
},
|
||
# FX - Efectos en el fondo de la mezcla
|
||
'reverse_fx': {
|
||
'volume': 0.52, # -7dB, efecto ambiente
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'riser': {
|
||
'volume': 0.60, # -5dB, sube hacia el climax
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'impact': {
|
||
'volume': 0.55, # -6dB, efecto puntual
|
||
'peak_reduction': 0.0,
|
||
},
|
||
'atmos': {
|
||
'volume': 0.50, # -8dB, fondo atmosferico
|
||
'peak_reduction': 0.0,
|
||
},
|
||
# VOCAL
|
||
'vocal': {
|
||
'volume': 0.70, # -3dB, debajo de drums pero presente
|
||
'peak_reduction': 0.0,
|
||
},
|
||
# SC TRIGGER - Track fantasma para sidechain
|
||
'sc_trigger': {
|
||
'volume': 0.0, # Sin salida de audio
|
||
'saturator_drive': 0.0,
|
||
'peak_reduction': 0.0,
|
||
},
|
||
}
|
||
|
||
# Factores de ajuste por estilo
|
||
# NOTA: NO usar multiplicadores de volumen que rompan el gain staging
|
||
# Solo ajustes sutiles de procesamiento y sends
|
||
STYLE_GAIN_ADJUSTMENTS = {
|
||
'industrial': {
|
||
'saturator_drive_factor': 1.3, # Aumentar drive en elementos agresivos
|
||
'additional_heat_send': 0.05, # Un poco mas de heat
|
||
'limiter_gain_factor': 1.15, # +15% gain para industrial techno
|
||
},
|
||
'latin': {
|
||
'additional_pan_width': 0.05,
|
||
},
|
||
'peak-time': {
|
||
'master_compressor_ratio_factor': 1.1,
|
||
'limiter_gain_factor': 1.1, # +10% gain para peak-time
|
||
},
|
||
'minimal': {
|
||
'fx_bus_send_reduction': 0.05,
|
||
'additional_space_send': 0.03, # Un poco mas de reverb para espacio
|
||
},
|
||
}
|
||
|
||
ROLE_BUS_ASSIGNMENTS = {
|
||
'sc_trigger': 'sc_trigger', # Rutea a su propio bus fantasma
|
||
'kick': 'drums',
|
||
'clap': 'drums',
|
||
'snare_fill': 'drums',
|
||
'hat_closed': 'drums',
|
||
'hat_open': 'drums',
|
||
'top_loop': 'drums',
|
||
'perc': 'drums',
|
||
'tom_fill': 'drums',
|
||
'ride': 'drums',
|
||
'crash': 'drums',
|
||
'sub_bass': 'bass',
|
||
'bass': 'bass',
|
||
'drone': 'music',
|
||
'chords': 'music',
|
||
'stab': 'music',
|
||
'pad': 'music',
|
||
'pluck': 'music',
|
||
'arp': 'music',
|
||
'lead': 'music',
|
||
'counter': 'music',
|
||
'reverse_fx': 'fx',
|
||
'riser': 'fx',
|
||
'impact': 'fx',
|
||
'atmos': 'fx',
|
||
'vocal': 'vocal',
|
||
}
|
||
|
||
SECTION_BLUEPRINTS = {
|
||
'minimal': [
|
||
('INTRO', 8, 12, 'intro', 1),
|
||
('GROOVE', 16, 20, 'build', 2),
|
||
('BREAK', 8, 25, 'break', 1),
|
||
('OUTRO', 8, 8, 'outro', 1),
|
||
],
|
||
'standard': [
|
||
('INTRO', 8, 12, 'intro', 1),
|
||
('BUILD', 8, 18, 'build', 2),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('BREAK', 8, 25, 'break', 1),
|
||
('DROP B', 16, 30, 'drop', 5),
|
||
('OUTRO', 8, 8, 'outro', 1),
|
||
],
|
||
'extended': [
|
||
('INTRO DJ', 16, 10, 'intro', 1),
|
||
('BUILD A', 8, 18, 'build', 2),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('BREAKDOWN', 8, 25, 'break', 1),
|
||
('BUILD B', 8, 18, 'build', 3),
|
||
('DROP B', 16, 30, 'drop', 5),
|
||
('OUTRO DJ', 16, 8, 'outro', 1),
|
||
],
|
||
'club': [
|
||
('INTRO DJ', 16, 10, 'intro', 1),
|
||
('GROOVE A', 16, 14, 'build', 2),
|
||
('VOCAL BUILD', 8, 18, 'build', 3),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('BREAKDOWN', 8, 25, 'break', 1),
|
||
('BUILD B', 8, 18, 'build', 3),
|
||
('DROP B', 16, 30, 'drop', 5),
|
||
('PEAK', 8, 32, 'drop', 5),
|
||
('OUTRO DJ', 16, 8, 'outro', 1),
|
||
],
|
||
'reggaeton': [
|
||
('INTRO', 8, 12, 'intro', 1),
|
||
('PRECORO', 8, 16, 'build', 2),
|
||
('CORO A', 16, 28, 'drop', 4),
|
||
('VERSEO', 16, 20, 'break', 2),
|
||
('PRECORO B', 8, 18, 'build', 3),
|
||
('CORO B', 16, 30, 'drop', 5),
|
||
('PUENTE', 8, 15, 'break', 1),
|
||
('CORO FINAL', 16, 32, 'drop', 5),
|
||
('OUTRO', 8, 10, 'outro', 1),
|
||
],
|
||
}
|
||
|
||
SECTION_BLUEPRINT_VARIANTS = {
|
||
'standard': [
|
||
SECTION_BLUEPRINTS['standard'],
|
||
[
|
||
('INTRO', 8, 12, 'intro', 1),
|
||
('GROOVE A', 8, 16, 'build', 2),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('BREAKDOWN', 8, 24, 'break', 1),
|
||
('BUILD B', 8, 20, 'build', 3),
|
||
('DROP B', 16, 31, 'drop', 5),
|
||
],
|
||
[
|
||
('INTRO DJ', 16, 10, 'intro', 1),
|
||
('BUILD', 8, 18, 'build', 2),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('MID BREAK', 8, 22, 'break', 1),
|
||
('PEAK', 16, 31, 'drop', 5),
|
||
],
|
||
],
|
||
'club': [
|
||
SECTION_BLUEPRINTS['club'],
|
||
[
|
||
('INTRO DJ', 16, 10, 'intro', 1),
|
||
('TEASE', 8, 14, 'build', 2),
|
||
('GROOVE A', 16, 18, 'build', 3),
|
||
('DROP A', 16, 28, 'drop', 4),
|
||
('BREAKDOWN', 8, 24, 'break', 1),
|
||
('BUILD B', 8, 20, 'build', 3),
|
||
('PEAK', 16, 32, 'drop', 5),
|
||
('OUTRO DJ', 24, 8, 'outro', 1),
|
||
],
|
||
[
|
||
('INTRO DJ', 16, 10, 'intro', 1),
|
||
('GROOVE A', 16, 15, 'build', 2),
|
||
('VOCAL BUILD', 8, 20, 'build', 3),
|
||
('DROP A', 16, 27, 'drop', 4),
|
||
('MID BREAK', 8, 22, 'break', 1),
|
||
('GROOVE B', 8, 18, 'build', 3),
|
||
('DROP B', 24, 31, 'drop', 5),
|
||
('OUTRO DJ', 16, 8, 'outro', 1),
|
||
],
|
||
],
|
||
}
|
||
|
||
ROLE_ACTIVITY = {
|
||
'sc_trigger': {'intro': 4, 'build': 4, 'drop': 4, 'break': 2, 'outro': 3},
|
||
'kick': {'intro': 2, 'build': 3, 'drop': 4, 'break': 1, 'outro': 2},
|
||
'clap': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},
|
||
'snare_fill': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},
|
||
'hat_closed': {'intro': 1, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},
|
||
'hat_open': {'intro': 0, 'build': 1, 'drop': 3, 'break': 0, 'outro': 1},
|
||
'top_loop': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},
|
||
'perc': {'intro': 0, 'build': 2, 'drop': 3, 'break': 1, 'outro': 0},
|
||
'tom_fill': {'intro': 0, 'build': 1, 'drop': 1, 'break': 0, 'outro': 0},
|
||
'ride': {'intro': 0, 'build': 1, 'drop': 2, 'break': 0, 'outro': 1},
|
||
'crash': {'intro': 0, 'build': 1, 'drop': 1, 'break': 0, 'outro': 0},
|
||
'sub_bass': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},
|
||
'bass': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 1},
|
||
'drone': {'intro': 2, 'build': 2, 'drop': 2, 'break': 3, 'outro': 2},
|
||
'chords': {'intro': 0, 'build': 2, 'drop': 3, 'break': 2, 'outro': 1},
|
||
'stab': {'intro': 0, 'build': 2, 'drop': 4, 'break': 1, 'outro': 0},
|
||
'pad': {'intro': 2, 'build': 2, 'drop': 2, 'break': 3, 'outro': 2},
|
||
'pluck': {'intro': 0, 'build': 2, 'drop': 3, 'break': 0, 'outro': 0},
|
||
'arp': {'intro': 0, 'build': 2, 'drop': 3, 'break': 1, 'outro': 0},
|
||
'lead': {'intro': 0, 'build': 1, 'drop': 4, 'break': 0, 'outro': 0},
|
||
'counter': {'intro': 0, 'build': 1, 'drop': 3, 'break': 1, 'outro': 0},
|
||
'reverse_fx': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},
|
||
'riser': {'intro': 0, 'build': 3, 'drop': 1, 'break': 2, 'outro': 0},
|
||
'impact': {'intro': 0, 'build': 2, 'drop': 1, 'break': 1, 'outro': 0},
|
||
'atmos': {'intro': 2, 'build': 1, 'drop': 1, 'break': 3, 'outro': 2},
|
||
'vocal': {'intro': 0, 'build': 1, 'drop': 2, 'break': 1, 'outro': 0},
|
||
}
|
||
|
||
# ROLE_MIX: Perfil de mezcla por rol
|
||
# Valores base que luego se calibran con ROLE_GAIN_CALIBRATION
|
||
# Volumenes calibrados relativos: kick = 0%, otros debajo
|
||
# Pan y sends optimizados para profundidad y espacio
|
||
ROLE_MIX = {
|
||
'sc_trigger': {'volume': 0.0, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.0}},
|
||
# DRUMS - Kick centered, elements below
|
||
'kick': {'volume': 0.85, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.08}},
|
||
'clap': {'volume': 0.78, 'pan': 0.0, 'sends': {'space': 0.14, 'echo': 0.04, 'heat': 0.02, 'glue': 0.10}},
|
||
'snare_fill': {'volume': 0.72, 'pan': 0.0, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.01, 'glue': 0.06}},
|
||
'hat_closed': {'volume': 0.68, 'pan': -0.10, 'sends': {'space': 0.04, 'echo': 0.03, 'heat': 0.0, 'glue': 0.04}},
|
||
'hat_open': {'volume': 0.65, 'pan': 0.12, 'sends': {'space': 0.10, 'echo': 0.08, 'heat': 0.01, 'glue': 0.06}},
|
||
'top_loop': {'volume': 0.62, 'pan': -0.16, 'sends': {'space': 0.06, 'echo': 0.12, 'heat': 0.0, 'glue': 0.08}},
|
||
'perc': {'volume': 0.70, 'pan': 0.20, 'sends': {'space': 0.10, 'echo': 0.14, 'heat': 0.02, 'glue': 0.10}},
|
||
'tom_fill': {'volume': 0.68, 'pan': 0.12, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.01, 'glue': 0.06}},
|
||
'ride': {'volume': 0.58, 'pan': 0.24, 'sends': {'space': 0.04, 'echo': 0.03, 'heat': 0.0, 'glue': 0.06}},
|
||
'crash': {'volume': 0.50, 'pan': 0.0, 'sends': {'space': 0.18, 'echo': 0.06, 'heat': 0.01, 'glue': 0.02}},
|
||
# BASS - Below drums, centered for mono compatibility
|
||
'sub_bass': {'volume': 0.80, 'pan': 0.0, 'sends': {'space': 0.0, 'echo': 0.0, 'heat': 0.0, 'glue': 0.14}},
|
||
'bass': {'volume': 0.78, 'pan': 0.0, 'sends': {'space': 0.01, 'echo': 0.01, 'heat': 0.04, 'glue': 0.12}},
|
||
# MUSIC - Layers below rhythm section
|
||
'drone': {'volume': 0.55, 'pan': 0.0, 'sends': {'space': 0.28, 'echo': 0.08, 'heat': 0.02, 'glue': 0.04}},
|
||
'chords': {'volume': 0.70, 'pan': -0.06, 'sends': {'space': 0.18, 'echo': 0.12, 'heat': 0.01, 'glue': 0.08}},
|
||
'stab': {'volume': 0.65, 'pan': 0.10, 'sends': {'space': 0.12, 'echo': 0.10, 'heat': 0.04, 'glue': 0.08}},
|
||
'pad': {'volume': 0.60, 'pan': -0.14, 'sends': {'space': 0.32, 'echo': 0.08, 'heat': 0.0, 'glue': 0.06}},
|
||
'pluck': {'volume': 0.68, 'pan': 0.14, 'sends': {'space': 0.08, 'echo': 0.18, 'heat': 0.01, 'glue': 0.06}},
|
||
'arp': {'volume': 0.65, 'pan': -0.18, 'sends': {'space': 0.14, 'echo': 0.24, 'heat': 0.01, 'glue': 0.08}},
|
||
'lead': {'volume': 0.72, 'pan': 0.06, 'sends': {'space': 0.14, 'echo': 0.18, 'heat': 0.03, 'glue': 0.10}},
|
||
'counter': {'volume': 0.62, 'pan': 0.20, 'sends': {'space': 0.18, 'echo': 0.14, 'heat': 0.01, 'glue': 0.06}},
|
||
# FX - Deep in the mix
|
||
'reverse_fx': {'volume': 0.52, 'pan': 0.0, 'sends': {'space': 0.24, 'echo': 0.10, 'heat': 0.03, 'glue': 0.02}},
|
||
'riser': {'volume': 0.60, 'pan': 0.0, 'sends': {'space': 0.28, 'echo': 0.14, 'heat': 0.04, 'glue': 0.03}},
|
||
'impact': {'volume': 0.55, 'pan': 0.0, 'sends': {'space': 0.22, 'echo': 0.12, 'heat': 0.01, 'glue': 0.03}},
|
||
'atmos': {'volume': 0.50, 'pan': -0.20, 'sends': {'space': 0.34, 'echo': 0.06, 'heat': 0.0, 'glue': 0.03}},
|
||
# VOCAL - Present but under drums
|
||
'vocal': {'volume': 0.70, 'pan': 0.08, 'sends': {'space': 0.20, 'echo': 0.24, 'heat': 0.02, 'glue': 0.10}},
|
||
}
|
||
|
||
ARRANGEMENT_PROFILES = (
|
||
{
|
||
'name': 'warehouse',
|
||
'genres': {'techno', 'tech-house'},
|
||
'drum_tightness': 1.15,
|
||
'bass_motion': 'locked',
|
||
'melodic_motion': 'restrained',
|
||
'pan_width': 0.12,
|
||
'fx_bias': 1.0,
|
||
},
|
||
{
|
||
'name': 'jackin',
|
||
'genres': {'house', 'tech-house'},
|
||
'drum_tightness': 0.96,
|
||
'bass_motion': 'bouncy',
|
||
'melodic_motion': 'call_response',
|
||
'pan_width': 0.16,
|
||
'fx_bias': 0.92,
|
||
},
|
||
{
|
||
'name': 'festival',
|
||
'genres': {'trance', 'house', 'tech-house'},
|
||
'drum_tightness': 0.92,
|
||
'bass_motion': 'lifted',
|
||
'melodic_motion': 'anthemic',
|
||
'pan_width': 0.2,
|
||
'fx_bias': 1.18,
|
||
},
|
||
{
|
||
'name': 'swing',
|
||
'genres': {'tech-house', 'house'},
|
||
'drum_tightness': 0.9,
|
||
'bass_motion': 'syncopated',
|
||
'melodic_motion': 'hooky',
|
||
'pan_width': 0.22,
|
||
'fx_bias': 1.05,
|
||
},
|
||
{
|
||
'name': 'tech-house-club',
|
||
'genres': {'tech-house'},
|
||
'drum_tightness': 0.94,
|
||
'bass_motion': 'bouncy',
|
||
'melodic_motion': 'hooky',
|
||
'pan_width': 0.18,
|
||
'fx_bias': 1.08,
|
||
'bus_names': {
|
||
'drums': 'DRUM CLUB',
|
||
'bass': 'BASS TUBE',
|
||
'music': 'MUSIC JACK',
|
||
'vocal': 'VOCAL LATIN BUS',
|
||
'fx': 'FX JAM',
|
||
},
|
||
'return_names': {
|
||
'space': 'REVERB SHORT',
|
||
'echo': 'DELAY MONO',
|
||
'heat': 'DRIVE HOT',
|
||
'glue': 'GLUE BUS',
|
||
},
|
||
},
|
||
{
|
||
'name': 'tech-house-deep',
|
||
'genres': {'tech-house'},
|
||
'drum_tightness': 1.02,
|
||
'bass_motion': 'locked',
|
||
'melodic_motion': 'restrained',
|
||
'pan_width': 0.14,
|
||
'fx_bias': 0.88,
|
||
'bus_names': {
|
||
'drums': 'DRUM DEEP',
|
||
'bass': 'SUB DEEP',
|
||
'music': 'ATMOS DEEP',
|
||
'vocal': 'VOX DEEP',
|
||
'fx': 'FX DEEP',
|
||
},
|
||
'return_names': {
|
||
'space': 'REVERB DEEP',
|
||
'echo': 'DELAY DEEP',
|
||
'heat': 'SATURATE DEEP',
|
||
'glue': 'GLUE MINIMAL',
|
||
},
|
||
},
|
||
{
|
||
'name': 'tech-house-funky',
|
||
'genres': {'tech-house'},
|
||
'drum_tightness': 0.86,
|
||
'bass_motion': 'syncopated',
|
||
'melodic_motion': 'hooky',
|
||
'pan_width': 0.24,
|
||
'fx_bias': 1.12,
|
||
'bus_names': {
|
||
'drums': 'DRUM GROOVE',
|
||
'bass': 'BASS FUNK',
|
||
'music': 'MUSIC GROOVE',
|
||
'vocal': 'VOCAL FUNK',
|
||
'fx': 'FX SWING',
|
||
},
|
||
'return_names': {
|
||
'space': 'REVERB GROOVE',
|
||
'echo': 'DELAY GROOVE',
|
||
'heat': 'DRIVE FUNK',
|
||
'glue': 'GLUE SWING',
|
||
},
|
||
},
|
||
{
|
||
'name': 'dembow',
|
||
'genres': {'reggaeton'},
|
||
'drum_tightness': 0.92,
|
||
'bass_motion': 'bouncy',
|
||
'melodic_motion': 'hooky',
|
||
'pan_width': 0.16,
|
||
'fx_bias': 0.95,
|
||
'bus_names': {
|
||
'drums': 'DRUM DEMBOW',
|
||
'bass': 'BASS TUBE',
|
||
'music': 'SYNTH PLUCK',
|
||
'vocal': 'VOCAL CHOP',
|
||
'fx': 'FX LATIN',
|
||
},
|
||
'return_names': {
|
||
'space': 'REVERB SHORT',
|
||
'echo': 'DELAY PING',
|
||
'heat': 'DRIVE HOT',
|
||
'glue': 'GLUE BUS',
|
||
},
|
||
},
|
||
{
|
||
'name': 'moombahton',
|
||
'genres': {'reggaeton'},
|
||
'drum_tightness': 0.88,
|
||
'bass_motion': 'heavy',
|
||
'melodic_motion': 'anthemic',
|
||
'pan_width': 0.20,
|
||
'fx_bias': 1.05,
|
||
'bus_names': {
|
||
'drums': 'DRUM MOOMBAH',
|
||
'bass': 'BASS HEAVY',
|
||
'music': 'SYNTH BIG',
|
||
'vocal': 'VOCAL LEAD',
|
||
'fx': 'FX FESTIVAL',
|
||
},
|
||
'return_names': {
|
||
'space': 'REVERB BIG',
|
||
'echo': 'DELAY WIDE',
|
||
'heat': 'DRIVE HEAVY',
|
||
'glue': 'GLUE FAT',
|
||
},
|
||
},
|
||
)
|
||
|
||
ROLE_FX_CHAINS = {
|
||
'sc_trigger': [
|
||
{'device': 'Utility', 'parameters': {'Gain': 0.0, 'Width': 0.0}},
|
||
],
|
||
'kick': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 2.5}},
|
||
],
|
||
'clap': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.08}},
|
||
],
|
||
'snare_fill': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}},
|
||
],
|
||
'hat_closed': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 15000.0, 'Dry/Wet': 0.14}},
|
||
],
|
||
'hat_open': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 12000.0, 'Dry/Wet': 0.18}},
|
||
],
|
||
'top_loop': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 11000.0, 'Dry/Wet': 0.22}},
|
||
],
|
||
'perc': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 9500.0, 'Dry/Wet': 0.16}},
|
||
],
|
||
'ride': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 12500.0, 'Dry/Wet': 0.12}},
|
||
],
|
||
'sub_bass': [
|
||
{'device': 'Utility', 'parameters': {'Width': 0.0}},
|
||
],
|
||
'bass': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 4.0}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 7800.0, 'Dry/Wet': 0.12}},
|
||
],
|
||
'drone': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.16}},
|
||
],
|
||
'chords': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 9800.0, 'Dry/Wet': 0.14}},
|
||
],
|
||
'stab': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 3.0}},
|
||
],
|
||
'pad': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.18}},
|
||
],
|
||
'pluck': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.12}},
|
||
],
|
||
'arp': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.16}},
|
||
],
|
||
'lead': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 2.0}},
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.12}},
|
||
],
|
||
'counter': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.1}},
|
||
],
|
||
'crash': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.16}},
|
||
],
|
||
'reverse_fx': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.24}},
|
||
],
|
||
'riser': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.28}},
|
||
],
|
||
'impact': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}},
|
||
],
|
||
'atmos': [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.3}},
|
||
],
|
||
'vocal': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.14}},
|
||
],
|
||
}
|
||
|
||
SCRIPTS_ROOT = Path(__file__).resolve().parents[2]
|
||
REFERENCE_SEARCH_DIRS = (
|
||
SCRIPTS_ROOT / 'sample',
|
||
SCRIPTS_ROOT / 'samples',
|
||
)
|
||
REFERENCE_TRACK_PROFILES = [
|
||
{
|
||
'name': 'Eli Brown x GeezLy - Me Gusta',
|
||
'match_terms': ['eli brown', 'geezly', 'me gusta'],
|
||
'genre': 'tech-house',
|
||
'style': 'latin-industrial',
|
||
'bpm': 136.0,
|
||
'key': 'F#m',
|
||
'structure': 'club',
|
||
'reference_bars': 112,
|
||
},
|
||
{
|
||
'name': 'Mr. Pauer, Goyo - QuÃmica',
|
||
'match_terms': ['mr. pauer', 'goyo', 'quÃmica'],
|
||
'genre': 'house',
|
||
'style': 'latin-funky vocal',
|
||
'bpm': 123.0,
|
||
'key': 'Cm',
|
||
'structure': 'extended',
|
||
'reference_bars': 72,
|
||
},
|
||
]
|
||
|
||
# =========================================================================
|
||
# SECTION AUTOMATION PARAMETERS
|
||
# =========================================================================
|
||
|
||
SECTION_AUTOMATION = {
|
||
'intro': {
|
||
'energy': 0.25,
|
||
'filters': {
|
||
'drums': {'frequency': 8500.0, 'resonance': 0.3, 'dry_wet': 0.12},
|
||
'bass': {'frequency': 6200.0, 'resonance': 0.25, 'dry_wet': 0.08},
|
||
'music': {'frequency': 7800.0, 'resonance': 0.2, 'dry_wet': 0.1},
|
||
'vocal': {'frequency': 9200.0, 'resonance': 0.15, 'dry_wet': 0.06},
|
||
'fx': {'frequency': 8800.0, 'resonance': 0.18, 'dry_wet': 0.14},
|
||
},
|
||
'reverb': {'send_level': 0.28, 'decay_time': 2.8, 'size': 0.85},
|
||
'delay': {'send_level': 0.18, 'feedback': 0.35, 'time_l': 0.375, 'time_r': 0.5},
|
||
'compression': {'threshold': -14.0, 'ratio': 2.0, 'attack': 0.015, 'release': 0.12},
|
||
'saturation': {'drive': 0.8, 'mix': 0.15},
|
||
'stereo_width': {'value': 0.92},
|
||
'envelope_curve': 'ease_in',
|
||
},
|
||
'build': {
|
||
'energy': 0.72,
|
||
'filters': {
|
||
'drums': {'frequency': 4200.0, 'resonance': 0.45, 'dry_wet': 0.22},
|
||
'bass': {'frequency': 3800.0, 'resonance': 0.35, 'dry_wet': 0.16},
|
||
'music': {'frequency': 5400.0, 'resonance': 0.28, 'dry_wet': 0.18},
|
||
'vocal': {'frequency': 6800.0, 'resonance': 0.22, 'dry_wet': 0.12},
|
||
'fx': {'frequency': 5200.0, 'resonance': 0.32, 'dry_wet': 0.24},
|
||
},
|
||
'reverb': {'send_level': 0.18, 'decay_time': 2.2, 'size': 0.72},
|
||
'delay': {'send_level': 0.32, 'feedback': 0.48, 'time_l': 0.375, 'time_r': 0.5},
|
||
'compression': {'threshold': -10.0, 'ratio': 3.5, 'attack': 0.008, 'release': 0.08},
|
||
'saturation': {'drive': 2.2, 'mix': 0.28},
|
||
'stereo_width': {'value': 1.08},
|
||
'envelope_curve': 'ramp_up',
|
||
},
|
||
'drop': {
|
||
'energy': 1.0,
|
||
'filters': {
|
||
'drums': {'frequency': 14500.0, 'resonance': 0.2, 'dry_wet': 0.04},
|
||
'bass': {'frequency': 9800.0, 'resonance': 0.15, 'dry_wet': 0.03},
|
||
'music': {'frequency': 12200.0, 'resonance': 0.12, 'dry_wet': 0.05},
|
||
'vocal': {'frequency': 12800.0, 'resonance': 0.1, 'dry_wet': 0.04},
|
||
'fx': {'frequency': 11000.0, 'resonance': 0.15, 'dry_wet': 0.08},
|
||
},
|
||
'reverb': {'send_level': 0.12, 'decay_time': 1.6, 'size': 0.55},
|
||
'delay': {'send_level': 0.14, 'feedback': 0.28, 'time_l': 0.25, 'time_r': 0.375},
|
||
'compression': {'threshold': -6.0, 'ratio': 4.5, 'attack': 0.005, 'release': 0.06},
|
||
'saturation': {'drive': 3.5, 'mix': 0.38},
|
||
'stereo_width': {'value': 1.18},
|
||
'envelope_curve': 'punch',
|
||
},
|
||
'break': {
|
||
'energy': 0.38,
|
||
'filters': {
|
||
'drums': {'frequency': 5200.0, 'resonance': 0.55, 'dry_wet': 0.32},
|
||
'bass': {'frequency': 2800.0, 'resonance': 0.45, 'dry_wet': 0.24},
|
||
'music': {'frequency': 6400.0, 'resonance': 0.35, 'dry_wet': 0.22},
|
||
'vocal': {'frequency': 8200.0, 'resonance': 0.28, 'dry_wet': 0.16},
|
||
'fx': {'frequency': 6800.0, 'resonance': 0.38, 'dry_wet': 0.28},
|
||
},
|
||
'reverb': {'send_level': 0.42, 'decay_time': 3.5, 'size': 1.0},
|
||
'delay': {'send_level': 0.38, 'feedback': 0.52, 'time_l': 0.5, 'time_r': 0.75},
|
||
'compression': {'threshold': -18.0, 'ratio': 1.8, 'attack': 0.025, 'release': 0.18},
|
||
'saturation': {'drive': 0.5, 'mix': 0.1},
|
||
'stereo_width': {'value': 1.25},
|
||
'envelope_curve': 'ease_out',
|
||
},
|
||
'outro': {
|
||
'energy': 0.32,
|
||
'filters': {
|
||
'drums': {'frequency': 6200.0, 'resonance': 0.35, 'dry_wet': 0.18},
|
||
'bass': {'frequency': 4200.0, 'resonance': 0.28, 'dry_wet': 0.14},
|
||
'music': {'frequency': 5600.0, 'resonance': 0.25, 'dry_wet': 0.16},
|
||
'vocal': {'frequency': 7200.0, 'resonance': 0.2, 'dry_wet': 0.1},
|
||
'fx': {'frequency': 6400.0, 'resonance': 0.28, 'dry_wet': 0.2},
|
||
},
|
||
'reverb': {'send_level': 0.35, 'decay_time': 3.2, 'size': 0.92},
|
||
'delay': {'send_level': 0.28, 'feedback': 0.42, 'time_l': 0.375, 'time_r': 0.5},
|
||
'compression': {'threshold': -12.0, 'ratio': 2.2, 'attack': 0.018, 'release': 0.15},
|
||
'saturation': {'drive': 0.6, 'mix': 0.12},
|
||
'stereo_width': {'value': 0.98},
|
||
'envelope_curve': 'ease_out',
|
||
},
|
||
}
|
||
|
||
# Envelope curve templates for automation interpolation
|
||
ENVELOPE_CURVES = {
|
||
'linear': lambda x: x,
|
||
'ease_in': lambda x: x * x,
|
||
'ease_out': lambda x: 1 - (1 - x) ** 2,
|
||
'ease_in_out': lambda x: 3 * x * x - 2 * x * x * x,
|
||
'ramp_up': lambda x: x ** 0.5,
|
||
'ramp_down': lambda x: 1 - (1 - x) ** 2,
|
||
'punch': lambda x: min(1.0, x * 2.0) if x < 0.5 else 1.0 - (1.0 - x) ** 0.5,
|
||
's_curve': lambda x: 1 / (1 + (2.71828 ** (-10 * (x - 0.5)))),
|
||
'exponential': lambda x: (2.71828 ** (x - 1) - 0.3679) / 0.6321,
|
||
}
|
||
|
||
# =============================================================================
|
||
# AUTOMATIZACION DE DEVICES POR SECCION - FASE 2
|
||
# Parametros especificos por device para cada tipo de seccion
|
||
# =============================================================================
|
||
|
||
# Automatizacion de devices en tracks individuales por rol - ENHANCED
|
||
SECTION_DEVICE_AUTOMATION = {
|
||
# BASS - Filtros, drive y compresion dinamica
|
||
'bass': {
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.5, 'build': 3.5, 'drop': 5.0, 'break': 2.0, 'outro': 1.8},
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.22, 'drop': 0.30, 'break': 0.15, 'outro': 0.10},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6200.0, 'build': 8500.0, 'drop': 12000.0, 'break': 4800.0, 'outro': 5800.0},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.18, 'drop': 0.12, 'break': 0.22, 'outro': 0.06},
|
||
'Resonance': {'intro': 0.25, 'build': 0.35, 'drop': 0.20, 'break': 0.40, 'outro': 0.28},
|
||
},
|
||
'Compressor': {
|
||
'Threshold': {'intro': -12.0, 'build': -14.0, 'drop': -18.0, 'break': -10.0, 'outro': -11.0},
|
||
'Ratio': {'intro': 2.5, 'build': 3.0, 'drop': 4.0, 'break': 2.0, 'outro': 2.2},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},
|
||
},
|
||
},
|
||
'sub_bass': {
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.0, 'build': 2.5, 'drop': 4.0, 'break': 1.5, 'outro': 1.2},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 5200.0, 'build': 7200.0, 'drop': 10000.0, 'break': 4200.0, 'outro': 4800.0},
|
||
'Dry/Wet': {'intro': 0.05, 'build': 0.10, 'drop': 0.06, 'break': 0.14, 'outro': 0.04},
|
||
},
|
||
'Utility': {
|
||
'Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},
|
||
'Gain': {'intro': 0.0, 'build': 0.2, 'drop': 0.4, 'break': -0.2, 'outro': 0.0},
|
||
},
|
||
},
|
||
# PAD - Filtros envolventes con width y reverb
|
||
'pad': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 4500.0, 'build': 8000.0, 'drop': 11000.0, 'break': 3200.0, 'outro': 4000.0},
|
||
'Dry/Wet': {'intro': 0.25, 'build': 0.18, 'drop': 0.12, 'break': 0.35, 'outro': 0.28},
|
||
'Resonance': {'intro': 0.20, 'build': 0.28, 'drop': 0.15, 'break': 0.35, 'outro': 0.22},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.22, 'build': 0.16, 'drop': 0.10, 'break': 0.28, 'outro': 0.24},
|
||
'Decay Time': {'intro': 3.5, 'build': 2.8, 'drop': 2.0, 'break': 4.2, 'outro': 3.8},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.85, 'build': 1.02, 'drop': 1.12, 'break': 1.25, 'outro': 0.90},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.8, 'build': 1.5, 'drop': 2.5, 'break': 0.6, 'outro': 0.7},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.15, 'drop': 0.20, 'break': 0.08, 'outro': 0.12},
|
||
},
|
||
},
|
||
# ATMOS - Filtros espaciales con movement
|
||
'atmos': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 3800.0, 'build': 7200.0, 'drop': 9800.0, 'break': 2800.0, 'outro': 3500.0},
|
||
'Dry/Wet': {'intro': 0.30, 'build': 0.22, 'drop': 0.15, 'break': 0.40, 'outro': 0.32},
|
||
'Resonance': {'intro': 0.22, 'build': 0.32, 'drop': 0.18, 'break': 0.42, 'outro': 0.25},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.35, 'build': 0.28, 'drop': 0.18, 'break': 0.42, 'outro': 0.38},
|
||
'Decay Time': {'intro': 4.0, 'build': 3.2, 'drop': 2.2, 'break': 5.0, 'outro': 4.5},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.70, 'build': 0.88, 'drop': 1.05, 'break': 1.20, 'outro': 0.75},
|
||
},
|
||
},
|
||
# FX ELEMENTS
|
||
'reverse_fx': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 5200.0, 'build': 9000.0, 'drop': 12000.0, 'break': 6000.0, 'outro': 4800.0},
|
||
'Dry/Wet': {'intro': 0.20, 'build': 0.28, 'drop': 0.15, 'break': 0.35, 'outro': 0.22},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.30, 'build': 0.35, 'drop': 0.20, 'break': 0.40, 'outro': 0.28},
|
||
'Decay Time': {'intro': 3.0, 'build': 4.5, 'drop': 2.5, 'break': 5.5, 'outro': 3.5},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.2, 'build': 2.8, 'drop': 4.5, 'break': 1.8, 'outro': 1.0},
|
||
},
|
||
},
|
||
'riser': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 4000.0, 'build': 10000.0, 'drop': 14000.0, 'break': 5500.0, 'outro': 4200.0},
|
||
'Dry/Wet': {'intro': 0.15, 'build': 0.30, 'drop': 0.12, 'break': 0.22, 'outro': 0.18},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.25, 'build': 0.40, 'drop': 0.22, 'break': 0.35, 'outro': 0.20},
|
||
'Decay Time': {'intro': 2.5, 'build': 5.0, 'drop': 3.0, 'break': 4.0, 'outro': 2.8},
|
||
},
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.18, 'build': 0.35, 'drop': 0.15, 'break': 0.25, 'outro': 0.15},
|
||
'Feedback': {'intro': 0.30, 'build': 0.55, 'drop': 0.25, 'break': 0.45, 'outro': 0.28},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.5, 'build': 4.0, 'drop': 3.0, 'break': 2.5, 'outro': 1.2},
|
||
},
|
||
},
|
||
'impact': {
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.15, 'build': 0.18, 'drop': 0.12, 'break': 0.20, 'outro': 0.14},
|
||
'Decay Time': {'intro': 2.0, 'build': 2.5, 'drop': 1.8, 'break': 3.0, 'outro': 2.2},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.8, 'build': 2.5, 'drop': 3.5, 'break': 2.0, 'outro': 1.5},
|
||
},
|
||
},
|
||
'drone': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 3000.0, 'build': 6500.0, 'drop': 9000.0, 'break': 2500.0, 'outro': 2800.0},
|
||
'Dry/Wet': {'intro': 0.20, 'build': 0.15, 'drop': 0.10, 'break': 0.30, 'outro': 0.22},
|
||
'Resonance': {'intro': 0.25, 'build': 0.35, 'drop': 0.22, 'break': 0.40, 'outro': 0.28},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.18, 'build': 0.14, 'drop': 0.08, 'break': 0.25, 'outro': 0.20},
|
||
'Decay Time': {'intro': 4.5, 'build': 3.5, 'drop': 2.5, 'break': 5.5, 'outro': 4.8},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.8, 'break': 0.6, 'outro': 0.7},
|
||
},
|
||
},
|
||
# HATS - Filtros de brillantez con resonance y saturacion
|
||
'hat_closed': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 12000.0, 'build': 14000.0, 'drop': 16000.0, 'break': 10000.0, 'outro': 11000.0},
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.16, 'drop': 0.10, 'break': 0.20, 'outro': 0.14},
|
||
'Resonance': {'intro': 0.15, 'build': 0.25, 'drop': 0.12, 'outro': 0.18, 'break': 0.30},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.5, 'build': 1.2, 'drop': 1.8, 'break': 0.8, 'outro': 0.6},
|
||
},
|
||
},
|
||
'hat_open': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 9000.0, 'build': 11000.0, 'drop': 13000.0, 'break': 7500.0, 'outro': 8500.0},
|
||
'Dry/Wet': {'intro': 0.18, 'build': 0.22, 'drop': 0.14, 'break': 0.28, 'outro': 0.20},
|
||
'Resonance': {'intro': 0.18, 'build': 0.28, 'drop': 0.15, 'outro': 0.20, 'break': 0.35},
|
||
},
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.22, 'outro': 0.12},
|
||
},
|
||
},
|
||
'top_loop': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 8500.0, 'build': 10500.0, 'drop': 12500.0, 'break': 7000.0, 'outro': 8000.0},
|
||
'Dry/Wet': {'intro': 0.20, 'build': 0.25, 'drop': 0.16, 'break': 0.32, 'outro': 0.22},
|
||
'Resonance': {'intro': 0.12, 'build': 0.22, 'drop': 0.14, 'outro': 0.15, 'break': 0.28},
|
||
},
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.05, 'build': 0.12, 'drop': 0.08, 'break': 0.18, 'outro': 0.10},
|
||
},
|
||
},
|
||
# SYNTHS
|
||
'chords': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 5500.0, 'build': 8500.0, 'drop': 11000.0, 'break': 4000.0, 'outro': 5000.0},
|
||
'Dry/Wet': {'intro': 0.15, 'build': 0.20, 'drop': 0.12, 'break': 0.28, 'outro': 0.18},
|
||
'Resonance': {'intro': 0.18, 'build': 0.28, 'drop': 0.15, 'outro': 0.20, 'break': 0.35},
|
||
},
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.08, 'break': 0.22, 'outro': 0.12},
|
||
'Feedback': {'intro': 0.25, 'build': 0.40, 'drop': 0.30, 'break': 0.45, 'outro': 0.28},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.2, 'build': 2.2, 'drop': 3.5, 'break': 1.5, 'outro': 1.0},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.95, 'build': 1.05, 'drop': 1.15, 'break': 1.25, 'outro': 1.00},
|
||
},
|
||
},
|
||
'lead': {
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.0, 'build': 2.5, 'drop': 4.0, 'break': 1.5, 'outro': 1.2},
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.20, 'drop': 0.25, 'break': 0.10, 'outro': 0.15},
|
||
},
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.18, 'outro': 0.10},
|
||
'Feedback': {'intro': 0.20, 'build': 0.35, 'drop': 0.28, 'break': 0.40, 'outro': 0.22},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6500.0, 'build': 9500.0, 'drop': 12500.0, 'break': 4500.0, 'outro': 5500.0},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.90, 'build': 1.02, 'drop': 1.10, 'break': 1.18, 'outro': 0.95},
|
||
},
|
||
},
|
||
'stab': {
|
||
'Saturator': {
|
||
'Drive': {'intro': 2.0, 'build': 3.5, 'drop': 5.0, 'break': 2.5, 'outro': 2.2},
|
||
'Dry/Wet': {'intro': 0.18, 'build': 0.25, 'drop': 0.30, 'break': 0.15, 'outro': 0.20},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6000.0, 'build': 9000.0, 'drop': 12000.0, 'break': 5000.0, 'outro': 5500.0},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.15, 'drop': 0.08, 'break': 0.22, 'outro': 0.12},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.88, 'build': 1.00, 'drop': 1.12, 'break': 1.20, 'outro': 0.92},
|
||
},
|
||
},
|
||
'pluck': {
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.22, 'drop': 0.14, 'break': 0.28, 'outro': 0.15},
|
||
'Feedback': {'intro': 0.30, 'build': 0.45, 'drop': 0.35, 'break': 0.50, 'outro': 0.32},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 7000.0, 'build': 10000.0, 'drop': 13000.0, 'break': 5500.0, 'outro': 6500.0},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.8, 'break': 1.2, 'outro': 0.9},
|
||
},
|
||
},
|
||
'arp': {
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.15, 'build': 0.28, 'drop': 0.18, 'break': 0.35, 'outro': 0.18},
|
||
'Feedback': {'intro': 0.35, 'build': 0.50, 'drop': 0.40, 'break': 0.58, 'outro': 0.38},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6500.0, 'build': 9500.0, 'drop': 12500.0, 'break': 5000.0, 'outro': 6000.0},
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.18, 'drop': 0.14, 'break': 0.25, 'outro': 0.15},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.6, 'build': 1.5, 'drop': 2.5, 'break': 1.0, 'outro': 0.7},
|
||
},
|
||
},
|
||
'counter': {
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.12, 'break': 0.22, 'outro': 0.12},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6000.0, 'build': 8800.0, 'drop': 11500.0, 'break': 4800.0, 'outro': 5200.0},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.16, 'drop': 0.12, 'break': 0.22, 'outro': 0.14},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.75, 'build': 0.92, 'drop': 1.08, 'break': 1.15, 'outro': 0.80},
|
||
},
|
||
},
|
||
# VOCAL
|
||
'vocal': {
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.25, 'drop': 0.15, 'break': 0.30, 'outro': 0.14},
|
||
'Feedback': {'intro': 0.25, 'build': 0.42, 'drop': 0.30, 'break': 0.48, 'outro': 0.28},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.06, 'break': 0.18, 'outro': 0.10},
|
||
'Decay Time': {'intro': 2.5, 'build': 3.5, 'drop': 2.0, 'break': 4.0, 'outro': 2.8},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6000.0, 'build': 9000.0, 'drop': 11000.0, 'break': 5000.0, 'outro': 5500.0},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.16, 'drop': 0.10, 'break': 0.20, 'outro': 0.12},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.8, 'build': 1.8, 'drop': 2.5, 'break': 1.2, 'outro': 0.9},
|
||
},
|
||
},
|
||
# DRUMS - Sin automatizacion de devices (manejados por volumen/sends)
|
||
'kick': {},
|
||
'clap': {},
|
||
'snare_fill': {},
|
||
'perc': {},
|
||
'ride': {},
|
||
'tom_fill': {},
|
||
'crash': {},
|
||
'sc_trigger': {},
|
||
}
|
||
|
||
# Automatizacion de devices en BUSES por seccion - ENHANCED
|
||
BUS_DEVICE_AUTOMATION = {
|
||
'drums': {
|
||
'Compressor': {
|
||
'Threshold': {'intro': -14.0, 'build': -16.0, 'drop': -18.5, 'break': -12.0, 'outro': -13.5},
|
||
'Ratio': {'intro': 2.5, 'build': 3.0, 'drop': 4.0, 'break': 2.2, 'outro': 2.4},
|
||
'Attack': {'intro': 0.015, 'build': 0.010, 'drop': 0.005, 'break': 0.020, 'outro': 0.018},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.8, 'build': 1.5, 'drop': 2.5, 'break': 1.0, 'outro': 0.9},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.22, 'break': 0.10, 'outro': 0.10},
|
||
},
|
||
'Limiter': {
|
||
'Gain': {'intro': 0.2, 'build': 0.3, 'drop': 0.5, 'break': 0.15, 'outro': 0.18},
|
||
},
|
||
'AutoFilter': {
|
||
'Frequency': {'intro': 8500.0, 'build': 12500.0, 'drop': 16000.0, 'break': 4500.0, 'outro': 6500.0},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.22, 'drop': 0.04, 'break': 0.35, 'outro': 0.18},
|
||
'Resonance': {'intro': 0.20, 'build': 0.12, 'drop': 0.08, 'break': 0.50, 'outro': 0.28},
|
||
},
|
||
},
|
||
'bass': {
|
||
'Saturator': {
|
||
'Drive': {'intro': 1.0, 'build': 2.0, 'drop': 3.5, 'break': 1.5, 'outro': 1.2},
|
||
'Dry/Wet': {'intro': 0.10, 'build': 0.18, 'drop': 0.25, 'break': 0.12, 'outro': 0.10},
|
||
},
|
||
'Compressor': {
|
||
'Threshold': {'intro': -15.0, 'build': -17.0, 'drop': -20.0, 'break': -14.0, 'outro': -14.5},
|
||
'Ratio': {'intro': 3.0, 'build': 3.5, 'drop': 4.5, 'break': 2.8, 'outro': 3.0},
|
||
'Attack': {'intro': 0.020, 'build': 0.015, 'drop': 0.008, 'break': 0.025, 'outro': 0.022},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 0.0, 'build': 0.0, 'drop': 0.0, 'break': 0.0, 'outro': 0.0},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 4800.0, 'build': 8500.0, 'drop': 12000.0, 'break': 3200.0, 'outro': 4200.0},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.05, 'break': 0.25, 'outro': 0.12},
|
||
'Resonance': {'intro': 0.18, 'build': 0.12, 'drop': 0.08, 'break': 0.45, 'outro': 0.22},
|
||
},
|
||
},
|
||
'music': {
|
||
'Compressor': {
|
||
'Threshold': {'intro': -19.0, 'build': -20.0, 'drop': -22.0, 'break': -18.0, 'outro': -18.5},
|
||
'Ratio': {'intro': 2.0, 'build': 2.5, 'drop': 3.0, 'break': 1.8, 'outro': 2.0},
|
||
'Attack': {'intro': 0.025, 'build': 0.020, 'drop': 0.015, 'break': 0.030, 'outro': 0.028},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 7500.0, 'build': 12000.0, 'drop': 16000.0, 'break': 4500.0, 'outro': 6000.0},
|
||
'Dry/Wet': {'intro': 0.12, 'build': 0.18, 'drop': 0.03, 'break': 0.30, 'outro': 0.15},
|
||
'Resonance': {'intro': 0.18, 'build': 0.10, 'drop': 0.06, 'break': 0.40, 'outro': 0.22},
|
||
},
|
||
'Utility': {
|
||
'Stereo Width': {'intro': 1.02, 'build': 1.08, 'drop': 1.12, 'break': 1.25, 'outro': 1.05},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.3, 'build': 0.8, 'drop': 1.5, 'break': 0.4, 'outro': 0.35},
|
||
'Dry/Wet': {'intro': 0.05, 'build': 0.10, 'drop': 0.15, 'break': 0.08, 'outro': 0.06},
|
||
},
|
||
},
|
||
'vocal': {
|
||
'Echo': {
|
||
'Dry/Wet': {'intro': 0.06, 'build': 0.12, 'drop': 0.05, 'break': 0.18, 'outro': 0.08},
|
||
'Feedback': {'intro': 0.25, 'build': 0.42, 'drop': 0.28, 'break': 0.50, 'outro': 0.30},
|
||
},
|
||
'Compressor': {
|
||
'Threshold': {'intro': -16.0, 'build': -17.0, 'drop': -19.0, 'break': -15.0, 'outro': -15.5},
|
||
'Ratio': {'intro': 2.8, 'build': 3.2, 'drop': 3.8, 'break': 2.5, 'outro': 2.7},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.06, 'build': 0.10, 'drop': 0.03, 'break': 0.16, 'outro': 0.08},
|
||
'Decay Time': {'intro': 2.2, 'build': 3.0, 'drop': 1.6, 'break': 4.0, 'outro': 2.5},
|
||
},
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 8000.0, 'build': 11500.0, 'drop': 14500.0, 'break': 6000.0, 'outro': 7200.0},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.12, 'drop': 0.04, 'break': 0.22, 'outro': 0.10},
|
||
'Resonance': {'intro': 0.15, 'build': 0.10, 'drop': 0.06, 'break': 0.32, 'outro': 0.18},
|
||
},
|
||
},
|
||
'fx': {
|
||
'Auto Filter': {
|
||
'Frequency': {'intro': 6000.0, 'build': 10500.0, 'drop': 14000.0, 'break': 4000.0, 'outro': 5200.0},
|
||
'Dry/Wet': {'intro': 0.15, 'build': 0.20, 'drop': 0.06, 'outro': 0.18, 'break': 0.35},
|
||
'Resonance': {'intro': 0.18, 'build': 0.15, 'drop': 0.10, 'break': 0.42, 'outro': 0.22},
|
||
},
|
||
'Hybrid Reverb': {
|
||
'Dry/Wet': {'intro': 0.20, 'build': 0.25, 'drop': 0.10, 'break': 0.38, 'outro': 0.22},
|
||
'Decay Time': {'intro': 3.0, 'build': 3.8, 'drop': 2.0, 'break': 5.0, 'outro': 3.5},
|
||
},
|
||
'Limiter': {
|
||
'Gain': {'intro': -0.3, 'build': 0.0, 'drop': 0.2, 'break': -0.5, 'outro': -0.2},
|
||
},
|
||
'Saturator': {
|
||
'Drive': {'intro': 0.5, 'build': 1.5, 'drop': 2.2, 'break': 0.8, 'outro': 0.6},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.14, 'drop': 0.20, 'break': 0.10, 'outro': 0.10},
|
||
},
|
||
},
|
||
}
|
||
|
||
# Automatizacion de devices en MASTER por seccion - ENHANCED
|
||
MASTER_DEVICE_AUTOMATION = {
|
||
'Utility': {'Stereo Width': {'intro': 1.04, 'build': 1.08, 'drop': 1.10, 'break': 1.12, 'outro': 1.06},
|
||
'Gain': {'intro': 0.72, 'build': 0.88, 'drop': 1.0, 'break': 0.68, 'outro': 0.70},
|
||
},
|
||
'Saturator': {'Drive': {'intro': 0.18, 'build': 0.30, 'drop': 0.45, 'break': 0.12, 'outro': 0.15},
|
||
'Dry/Wet': {'intro': 0.08, 'build': 0.15, 'drop': 0.22, 'break': 0.06, 'outro': 0.10},
|
||
},
|
||
'Compressor': {'Ratio': {'intro': 0.55, 'build': 0.62, 'drop': 0.68, 'break': 0.50, 'outro': 0.52},
|
||
'Threshold': {'intro': -10.0, 'build': -12.0, 'drop': -13.5, 'break': -8.0, 'outro': -9.0},
|
||
'Attack': {'intro': 0.020, 'build': 0.015, 'drop': 0.010, 'break': 0.025, 'outro': 0.022},
|
||
'Release': {'intro': 0.15, 'build': 0.12, 'drop': 0.10, 'break': 0.18, 'outro': 0.16},
|
||
},
|
||
'Limiter': {'Gain': {'intro': 1.05, 'build': 1.12, 'drop': 1.20, 'break': 1.00, 'outro': 1.02},
|
||
'Ceiling': {'intro': -0.5, 'build': -0.7, 'drop': -0.9, 'break': -0.4, 'outro': -0.45},
|
||
},
|
||
'Auto Filter': {'Frequency': {'intro': 8500.0, 'build': 12000.0, 'drop': 16000.0, 'break': 5500.0, 'outro': 7500.0},
|
||
'Dry/Wet': {'intro': 0.04, 'build': 0.02, 'drop': 0.01, 'break': 0.06, 'outro': 0.05},
|
||
},
|
||
'Echo': {'Dry/Wet': {'intro': 0.02, 'build': 0.05, 'drop': 0.03, 'break': 0.07, 'outro': 0.03},
|
||
'Feedback': {'intro': 0.15, 'build': 0.25, 'drop': 0.18, 'break': 0.30, 'outro': 0.20},
|
||
},
|
||
}
|
||
|
||
DEVICE_PARAMETER_SAFETY_CLAMPS = {
|
||
'Drive': {'min': 0.0, 'max': 6.0},
|
||
'Frequency': {'min': 20.0, 'max': 20000.0},
|
||
'Dry/Wet': {'min': 0.0, 'max': 1.0},
|
||
'Feedback': {'min': 0.0, 'max': 0.7},
|
||
'Stereo Width': {'min': 0.0, 'max': 1.3},
|
||
'Resonance': {'min': 0.0, 'max': 1.0},
|
||
'Ratio': {'min': 1.0, 'max': 20.0},
|
||
'Threshold': {'min': -60.0, 'max': 0.0},
|
||
'Attack': {'min': 0.0001, 'max': 0.5},
|
||
'Release': {'min': 0.001, 'max': 2.0},
|
||
'Gain': {'min': -1.0, 'max': 1.8},
|
||
'Decay Time': {'min': 0.1, 'max': 10.0},
|
||
}
|
||
|
||
MASTER_SAFETY_CLAMPS = {
|
||
'Stereo Width': {'min': 0.0, 'max': 1.25},
|
||
'Drive': {'min': 0.0, 'max': 1.5},
|
||
'Ratio': {'min': 0.45, 'max': 0.9},
|
||
'Gain': {'min': 0.0, 'max': 1.6},
|
||
'Attack': {'min': 0.0001, 'max': 0.1},
|
||
'Ceiling': {'min': -3.0, 'max': 0.0},
|
||
'Threshold': {'min': -20.0, 'max': 0.0},
|
||
'Release': {'min': 0.001, 'max': 1.0},
|
||
}
|
||
|
||
# Expanded configuration de variación por sección
|
||
SECTION_VARIATION_CONFIG = {
|
||
'perc': {
|
||
'intro': {'sparse': True, 'intensity': 0.3, 'variant': 'ghost'},
|
||
'build': {'building': True, 'intensity': 0.8, 'variant': 'layering'},
|
||
'drop': {'full': True, 'intensity': 1.0, 'variant': 'layered'},
|
||
'break': {'sparse': True, 'intensity': 0.4, 'variant': 'minimal'},
|
||
'outro': {'fading': True, 'intensity': 0.3, 'variant': 'strip_down'},
|
||
},
|
||
'perc_alt': {
|
||
'intro': {'sparse': True, 'intensity': 0.2, 'variant': 'minimal'},
|
||
'build': {'building': True, 'intensity': 0.6, 'variant': 'tension'},
|
||
'drop': {'full': True, 'intensity': 0.7, 'variant': 'groove'},
|
||
'break': {'sparse': True, 'intensity': 0.3, 'variant': 'atmos'},
|
||
'outro': {'fading': True, 'intensity': 0.2, 'variant': 'minimal'},
|
||
},
|
||
'top_loop': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'intensity': 0.8, 'variant': 'energy'},
|
||
'drop': {'full': True, 'intensity': 1.0, 'variant': 'full'},
|
||
'break': {'sparse': True, 'intensity': 0.4, 'variant': 'filtered'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'hat_open': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'intensity': 0.7, 'variant': 'tease'},
|
||
'drop': {'full': True, 'intensity': 0.9, 'variant': 'offbeat'},
|
||
'break': {'sparse': True, 'intensity': 0.3, 'variant': 'filtered'},
|
||
'outro': {'fading': True, 'intensity': 0.4, 'variant': 'fading'},
|
||
},
|
||
'ride': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'intensity': 0.6, 'variant': 'building'},
|
||
'drop': {'full': True, 'intensity': 0.8, 'variant': 'full'},
|
||
'break': {'sparse': True, 'intensity': 0.3, 'variant': 'sparse'},
|
||
'outro': {'fading': True, 'intensity': 0.4, 'variant': 'minimal'},
|
||
},
|
||
'snare_fill': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'tension': True, 'intensity': 0.8, 'variant': 'rolling'},
|
||
'drop': {'impact': True, 'intensity': 0.6, 'variant': 'fill'},
|
||
'break': {'sparse': True, 'intensity': 0.5, 'variant': 'tension'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'tom_fill': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'rising': True, 'intensity': 0.7, 'variant': 'rising'},
|
||
'drop': {'impact': True, 'intensity': 0.5, 'variant': 'fill'},
|
||
'break': {'use': False, 'variant': 'absent'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'vocal_shot': {
|
||
'intro': {'sparse': True, 'variant': 'hint'},
|
||
'build': {'building': True, 'variant': 'anticipate'},
|
||
'drop': {'full': True, 'variant': 'hook'},
|
||
'break': {'sparse': True, 'variant': 'filtered'},
|
||
'outro': {'fading': True, 'variant': 'minimal'},
|
||
},
|
||
'synth_peak': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'variant': 'rising'},
|
||
'drop': {'full': True, 'variant': 'anthem'},
|
||
'break': {'use': False, 'variant': 'absent'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'atmos': {
|
||
'intro': {'full': True, 'decay': 'long', 'variant': 'atmospheric'},
|
||
'build': {'building': True, 'variant': 'tension'},
|
||
'drop': {'sparse': True, 'variant': 'minimal'},
|
||
'break': {'full': True, 'decay': 'long', 'variant': 'ethereal'},
|
||
'outro': {'fading': True, 'decay': 'long', 'variant': 'fading'},
|
||
},
|
||
'chords': {
|
||
'intro': {'sparse': True, 'variant': 'foreshadow'},
|
||
'build': {'building': True, 'variant': 'rising'},
|
||
'drop': {'full': True, 'variant': 'full'},
|
||
'break': {'sparse': True, 'variant': 'atmospheric'},
|
||
'outro': {'fading': True, 'variant': 'echo'},
|
||
},
|
||
'pad': {
|
||
'intro': {'full': True, 'variant': 'atmospheric'},
|
||
'build': {'building': True, 'variant': 'tension'},
|
||
'drop': {'sparse': True, 'variant': 'minimal'},
|
||
'break': {'full': True, 'variant': 'ethereal'},
|
||
'outro': {'fading': True, 'variant': 'decay'},
|
||
},
|
||
'lead': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'variant': 'rising'},
|
||
'drop': {'full': True, 'variant': 'hook'},
|
||
'break': {'sparse': True, 'variant': 'minimal'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'arp': {
|
||
'intro': {'sparse': True, 'variant': 'ghost'},
|
||
'build': {'building': True, 'variant': 'energy'},
|
||
'drop': {'full': True, 'variant': 'driving'},
|
||
'break': {'sparse': True, 'variant': 'filtered'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'pluck': {
|
||
'intro': {'sparse': True, 'variant': 'hint'},
|
||
'build': {'building': True, 'variant': 'tension'},
|
||
'drop': {'full': True, 'variant': 'punchy'},
|
||
'break': {'sparse': True, 'variant': 'minimal'},
|
||
'outro': {'fading': True, 'variant': 'strip_down'},
|
||
},
|
||
'bass': {
|
||
'intro': {'sparse': True, 'variant': 'subtle'},
|
||
'build': {'building': True, 'variant': 'rising'},
|
||
'drop': {'full': True, 'variant': 'groove'},
|
||
'break': {'sparse': True, 'variant': 'filtered'},
|
||
'outro': {'fading': True, 'variant': 'fading'},
|
||
},
|
||
'sub_bass': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'building': True, 'variant': 'hint'},
|
||
'drop': {'full': True, 'variant': 'deep'},
|
||
'break': {'sparse': True, 'variant': 'minimal'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
'stab': {
|
||
'intro': {'use': False, 'variant': 'absent'},
|
||
'build': {'sparse': True, 'variant': 'hint'},
|
||
'drop': {'full': True, 'variant': 'impact'},
|
||
'break': {'use': False, 'variant': 'absent'},
|
||
'outro': {'use': False, 'variant': 'absent'},
|
||
},
|
||
}
|
||
|
||
# =========================================================================
|
||
# PATTERN VARIATION SYSTEM - Anti-repetition tracking
|
||
# =========================================================================
|
||
|
||
class PatternVariationManager:
|
||
"""
|
||
Manages pattern variant selection with cross-generation memory
|
||
to prevent repetitive patterns across sections and generations.
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.memory: Dict[str, Dict[str, int]] = {
|
||
'drum': {},
|
||
'bass': {},
|
||
'melodic': {},
|
||
}
|
||
self.section_signatures: List[str] = []
|
||
self.max_memory_age = 5 # Generations before decay
|
||
|
||
def record_usage(self, category: str, variant: str) -> None:
|
||
"""Record that a pattern variant was used."""
|
||
if category not in self.memory:
|
||
self.memory[category] = {}
|
||
self.memory[category][variant] = self.memory[category].get(variant, 0) + 1
|
||
logger.debug(f"[PATTERN_MEMORY] Recorded {category}:{variant} (count: {self.memory[category][variant]})")
|
||
|
||
def get_penalty(self, category: str, variant: str) -> float:
|
||
"""Get penalty score for a variant based on recent usage."""
|
||
count = self.memory.get(category, {}).get(variant, 0)
|
||
penalty = min(0.4, count * 0.08) # Max 40% penalty
|
||
if penalty > 0:
|
||
logger.debug(f"[PATTERN_MEMORY] Penalty for {category}:{variant} = {penalty:.2f} (used {count}x)")
|
||
return penalty
|
||
|
||
def decay_memory(self) -> None:
|
||
"""Decay memory to allow reuse after generations."""
|
||
for category in self.memory:
|
||
for variant in list(self.memory[category].keys()):
|
||
self.memory[category][variant] = max(0, self.memory[category][variant] - 1)
|
||
if self.memory[category][variant] <= 0:
|
||
del self.memory[category][variant]
|
||
|
||
def reset(self) -> None:
|
||
"""Reset all memory."""
|
||
self.memory = {'drum': {}, 'bass': {}, 'melodic': {}}
|
||
self.section_signatures = []
|
||
logger.info("[PATTERN_MEMORY] Reset all pattern variant memory")
|
||
|
||
def compute_section_signature(self, section: Dict[str, Any]) -> str:
|
||
"""Compute a signature for section to detect repetition."""
|
||
drum_variants = section.get('drum_role_variants', {})
|
||
signature_parts = [
|
||
f"k:{drum_variants.get('kick', 'default')}",
|
||
f"c:{drum_variants.get('clap', 'default')}",
|
||
f"h:{drum_variants.get('hat_closed', 'default')}",
|
||
f"b:{section.get('bass_bank_variant', 'anchor')}",
|
||
f"m:{section.get('melodic_bank_variant', 'motif')}",
|
||
f"d:{section.get('density', 1.0):.1f}",
|
||
]
|
||
return "|".join(signature_parts)
|
||
|
||
def check_repetition(self, sections: List[Dict[str, Any]]) -> List[Tuple[int, str]]:
|
||
"""Check for repetitive sections and return warnings."""
|
||
warnings = []
|
||
signatures = []
|
||
consecutive_same = 0
|
||
|
||
for i, section in enumerate(sections):
|
||
sig = self.compute_section_signature(section)
|
||
signatures.append(sig)
|
||
|
||
if signatures and len(signatures) > 1 and signatures[-2] == sig:
|
||
consecutive_same += 1
|
||
if consecutive_same >= 2:
|
||
warning_msg = f"[REPETITION_DETECTED] Sections {i-1}-{i} have identical signature: {sig}"
|
||
logger.warning(warning_msg)
|
||
warnings.append((i, sig))
|
||
else:
|
||
consecutive_same = 0
|
||
|
||
return warnings
|
||
|
||
# Global pattern variation manager
|
||
_pattern_variation_manager = PatternVariationManager()
|
||
|
||
def get_pattern_manager() -> PatternVariationManager:
|
||
"""Get the global pattern variation manager."""
|
||
return _pattern_variation_manager
|
||
|
||
# Legacy compatibility functions
|
||
def _get_pattern_variant_penalty(category: str, variant: str) -> float:
|
||
"""Get penalty for a pattern variant (legacy wrapper)."""
|
||
return _pattern_variation_manager.get_penalty(category, variant)
|
||
|
||
def _record_pattern_variant_usage(category: str, variant: str) -> None:
|
||
"""Record pattern variant usage (legacy wrapper)."""
|
||
_pattern_variation_manager.record_usage(category, variant)
|
||
|
||
def _decay_pattern_variant_memory() -> None:
|
||
"""Decay pattern variant memory (legacy wrapper)."""
|
||
_pattern_variation_manager.decay_memory()
|
||
|
||
def reset_pattern_variant_memory() -> None:
|
||
"""Reset all pattern variant memory (legacy wrapper)."""
|
||
_pattern_variation_manager.reset()
|
||
|
||
|
||
# =============================================================================
|
||
# DRUM PATTERN BANKS - Expanded Section-Specific Variants (11+ kick, 10+ clap, 8+ hat)
|
||
# =============================================================================
|
||
|
||
# Section-specific drum variants mapping - EXPANDED with 11+ kick, 10+ clap, 8+ hat variants
|
||
DRUM_SECTION_VARIANTS = {
|
||
'intro': {
|
||
# KICK: 11 variants - minimal, ghost notes, filtered, etc.
|
||
'kick': ['sparse', 'minimal', 'foreshadow', 'hint', 'ghost', 'filtered', 'subtle', 'pulse', 'sub_bass', 'tick', 'heartbeat'],
|
||
# CLAP: 10 variants
|
||
'clap': ['absent', 'hint', 'ghost', 'filtered', 'reverb_tail', 'minimal', 'subtle', 'single', 'distant', 'echo'],
|
||
# HAT: 8+ variants
|
||
'hat_closed': ['sparse', 'ghost', 'whisper', 'filtered', 'minimal', 'reverb_tail', 'subtle', 'tick'],
|
||
'hat_open': ['absent', 'hint', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'tick', 'single'],
|
||
'perc': ['minimal', 'atmos', 'ghost', 'subtle', 'filtered', 'tick', 'reverb_tail', 'sparse'],
|
||
'ride': ['absent', 'hint', 'subtle', 'minimal', 'filtered', 'ghost'],
|
||
'top_loop': ['absent', 'hint', 'filtered', 'minimal', 'subtle', 'ghost'],
|
||
'snare_fill': ['absent', 'hint', 'ghost', 'minimal'],
|
||
'tom_fill': ['absent', 'hint', 'ghost', 'filtered'],
|
||
},
|
||
'build': {
|
||
# KICK: 11 variants - building energy
|
||
'kick': ['building', 'pressure', 'rising', 'tension', 'accelerate', 'filter_sweep', 'drive_up', 'tighten', 'fill_preparation', 'intensity', 'impact_build'],
|
||
# CLAP: 10 variants
|
||
'clap': ['building', 'anticipate', 'roll_in', 'intensify', 'echo_build', 'filter_sweep', 'layering', 'reverb_up', 'drive_up', 'accelerate'],
|
||
'hat_closed': ['building', 'open_up', 'hyper', 'intensify', 'filter_sweep', 'accelerate', 'reverb_up', 'layering'],
|
||
'hat_open': ['building', 'tease', 'accent', 'filter_sweep', 'intensify', 'fill_preparation', 'open_build'],
|
||
'perc': ['layering', 'tension', 'build_up', 'intensify', 'accelerate', 'filter_sweep', 'reverb_up', 'drive_up'],
|
||
'ride': ['building', 'rising', 'intensify', 'filter_sweep', 'reverb_up', 'accelerate'],
|
||
'top_loop': ['building', 'energy', 'intensify', 'filter_sweep', 'drive_up', 'layering'],
|
||
'snare_fill': ['rolling', 'tension', 'accelerate', 'intensify', 'fill_preparation'],
|
||
'tom_fill': ['rising', 'fill', 'intensify', 'accelerate', 'fill_preparation'],
|
||
},
|
||
'drop': {
|
||
# KICK: 11 variants - full energy patterns
|
||
'kick': ['full', 'punch', 'four_on_floor', 'groove', 'impact', 'heavy', 'driving', 'tight', 'big_room', 'club', 'techno_thump'],
|
||
# CLAP: 10 variants
|
||
'clap': ['full', 'backbeat', 'syncopated', 'punch', 'big', 'layered', 'room', 'tight', 'crisp', 'slap'],
|
||
'hat_closed': ['full', 'groove', 'offbeat', 'shuffle', 'tight', 'driving', 'punchy', 'crisp'],
|
||
'hat_open': ['full', 'offbeat', 'groove', 'accent', 'big', 'room', 'open_drive', 'shuffle'],
|
||
'perc': ['full', 'layered', 'groove', 'latin', 'tribal', 'driving', 'tight', 'energetic'],
|
||
'ride': ['full', 'groove', 'energy', 'driving', 'tight', 'shimmer'],
|
||
'top_loop': ['full', 'energy', 'layered', 'driving', 'tight', 'groove'],
|
||
'snare_fill': ['drop_hit', 'fill', 'impact', 'big', 'accent'],
|
||
'tom_fill': ['drop_hit', 'fill', 'impact', 'big', 'accent'],
|
||
},
|
||
'break': {
|
||
# KICK: 11 variants - stripped down
|
||
'kick': ['sparse', 'absent', 'minimal', 'foreshadow', 'ghost', 'filtered', 'subtle', 'heartbeat', 'pulse', 'distant', 'reverb_only'],
|
||
# CLAP: 10 variants
|
||
'clap': ['sparse', 'offbeat', 'ghost', 'filtered', 'reverb_tail', 'minimal', 'subtle', 'distant', 'echo', 'single'],
|
||
'hat_closed': ['open', 'sparse', 'atmos', 'filtered', 'minimal', 'reverb_tail', 'subtle', 'ghost'],
|
||
'hat_open': ['sparse', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'subtle', 'atmos', 'distant'],
|
||
'perc': ['minimal', 'atmos', 'filtered', 'ghost', 'reverb_tail', 'subtle', 'sparse', 'distant'],
|
||
'ride': ['sparse', 'filtered', 'minimal', 'ghost', 'reverb_tail', 'subtle'],
|
||
'top_loop': ['filtered', 'hint', 'minimal', 'ghost', 'reverb_tail', 'subtle'],
|
||
'snare_fill': ['tension', 'ghost', 'minimal', 'filtered', 'echo'],
|
||
'tom_fill': ['tension', 'ghost', 'minimal', 'filtered', 'echo'],
|
||
},
|
||
'outro': {
|
||
# KICK: 11 variants - fading out
|
||
'kick': ['fading', 'minimal', 'sparse', 'strip_down', 'reverb_tail', 'heartbeat', 'subtle', 'distant', 'filtered', 'pulse', 'fade'],
|
||
# CLAP: 10 variants
|
||
'clap': ['fading', 'sparse', 'last_hit', 'minimal', 'reverb_tail', 'distant', 'echo', 'subtle', 'ghost', 'filtered'],
|
||
'hat_closed': ['fading', 'open', 'minimal', 'reverb_tail', 'subtle', 'sparse', 'ghost', 'filtered'],
|
||
'hat_open': ['fading', 'last_hit', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'distant', 'filtered'],
|
||
'perc': ['fading', 'minimal', 'strip_down', 'reverb_tail', 'subtle', 'sparse', 'ghost', 'filtered'],
|
||
'ride': ['fading', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'filtered'],
|
||
'top_loop': ['fading', 'minimal', 'reverb_tail', 'subtle', 'ghost', 'filtered'],
|
||
'snare_fill': ['end_fill', 'absent', 'minimal', 'reverb_tail', 'ghost'],
|
||
'tom_fill': ['end_fill', 'absent', 'minimal', 'reverb_tail', 'ghost'],
|
||
},
|
||
}
|
||
|
||
# TODO-008: REGGAETON_SECTION_VARIANTS - Variantes especificas para reggaeton
|
||
# Mapeo de roles a variantes por tipo de seccion para reggaeton
|
||
REGGAETON_SECTION_VARIANTS = {
|
||
'bass': {
|
||
'intro': {'variant': 'smooth deep', 'intensity': 0.6},
|
||
'build': {'variant': 'rising', 'intensity': 0.8},
|
||
'drop': {'variant': 'full punchy dembow', 'intensity': 1.0},
|
||
'break': {'variant': 'minimal rolling', 'intensity': 0.5},
|
||
'outro': {'variant': 'atmospheric filtered', 'intensity': 0.4},
|
||
},
|
||
'perc': {
|
||
'intro': {'variant': 'minimal', 'intensity': 0.3},
|
||
'build': {'variant': 'layering', 'intensity': 0.7},
|
||
'drop': {'variant': 'full dembow latin percussion', 'intensity': 1.0},
|
||
'break': {'variant': 'sparse congas bongos', 'intensity': 0.4},
|
||
'outro': {'variant': 'minimal', 'intensity': 0.2},
|
||
},
|
||
'vocal': {
|
||
'intro': {'variant': 'absent', 'intensity': 0.0},
|
||
'build': {'variant': 'tease', 'intensity': 0.5},
|
||
'drop': {'variant': 'full chop', 'intensity': 1.0},
|
||
'break': {'variant': 'phrase', 'intensity': 0.7},
|
||
'outro': {'variant': 'fade', 'intensity': 0.3},
|
||
},
|
||
'synth': {
|
||
'intro': {'variant': 'filtered', 'intensity': 0.4},
|
||
'build': {'variant': 'rising', 'intensity': 0.8},
|
||
'drop': {'variant': 'pluck hooky', 'intensity': 1.0},
|
||
'break': {'variant': 'pad atmospheric', 'intensity': 0.5},
|
||
'outro': {'variant': 'fade', 'intensity': 0.3},
|
||
},
|
||
}
|
||
|
||
# Expanded drum pattern generators for section variation
|
||
DRUM_PATTERN_BANKS = {
|
||
'kick': {
|
||
'four_on_floor': [0.0, 1.0, 2.0, 3.0],
|
||
'sparse': [0.0, 2.0],
|
||
'minimal': [0.0],
|
||
'foreshadow': [0.0, 3.5],
|
||
'hint': [0.0, 2.5],
|
||
'building': [0.0, 1.0, 2.0, 3.0, 3.5],
|
||
'pressure': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'rising': [0.0, 1.0, 2.0, 2.75, 3.0, 3.25, 3.5, 3.75],
|
||
'tension': [0.0, 0.25, 1.0, 1.5, 2.0, 2.75, 3.0, 3.25, 3.5],
|
||
'full': [0.0, 1.0, 2.0, 3.0],
|
||
'punch': [0.0, 0.25, 1.0, 2.0, 3.0],
|
||
'groove': [0.0, 0.75, 1.0, 1.75, 2.0, 2.75, 3.0, 3.75],
|
||
'impact': [0.0, 0.25, 0.5, 1.0, 2.0, 3.0],
|
||
'fading': [0.0, 2.0],
|
||
'strip_down': [0.0],
|
||
'absent': [],
|
||
},
|
||
'clap': {
|
||
'backbeat': [1.0, 3.0],
|
||
'sparse': [1.0],
|
||
'hint': [3.0],
|
||
'building': [1.0, 2.5, 3.0],
|
||
'anticipate': [1.0, 2.0, 2.75, 3.0, 3.5],
|
||
'roll_in': [0.75, 1.0, 1.25, 1.5, 2.75, 3.0, 3.25, 3.5],
|
||
'full': [1.0, 3.0],
|
||
'syncopated': [0.75, 1.0, 2.75, 3.0],
|
||
'offbeat': [1.5, 3.5],
|
||
'punch': [0.75, 1.0, 1.25, 2.75, 3.0, 3.25],
|
||
'ghost': [3.0],
|
||
'last_hit': [1.0],
|
||
'fading': [1.0],
|
||
'absent': [],
|
||
},
|
||
'hat_closed': {
|
||
'offbeat': [0.5, 1.5, 2.5, 3.5],
|
||
'sparse': [0.5, 2.5],
|
||
'ghost': [0.25, 1.25, 2.25, 3.25],
|
||
'whisper': [0.75, 1.75, 2.75, 3.75],
|
||
'building': [0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'open_up': [0.5, 0.75, 1.5, 1.75, 2.5, 2.75, 3.5, 3.75],
|
||
'hyper': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75],
|
||
'full': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'groove': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'shuffle': [0.0, 0.33, 0.66, 1.0, 1.33, 1.66, 2.0, 2.33, 2.66, 3.0, 3.33, 3.66],
|
||
'filtered': [0.5, 1.5, 2.5, 3.5],
|
||
'energy': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'fading': [0.5, 2.5],
|
||
'minimal': [0.5],
|
||
},
|
||
'hat_open': {
|
||
'sparse': [2.0],
|
||
'building': [1.5, 2.5, 3.0],
|
||
'full': [0.0, 2.0],
|
||
'offbeat': [1.5, 3.5],
|
||
'tease': [3.5],
|
||
'fading': [2.0],
|
||
'last_hit': [3.5],
|
||
'hint': [2.0],
|
||
'absent': [],
|
||
},
|
||
'perc': {
|
||
'minimal': [1.5],
|
||
'atmos': [0.75, 2.75],
|
||
'ghost': [0.25, 2.25],
|
||
'layering': [0.5, 1.5, 2.5, 3.5],
|
||
'tension': [0.25, 1.25, 2.25, 3.25],
|
||
'build_up': [0.5, 1.0, 2.0, 3.0, 3.5],
|
||
'full': [0.5, 1.0, 1.5, 2.5, 3.0, 3.5],
|
||
'layered': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],
|
||
'groove': [0.5, 1.0, 2.0, 2.5, 3.5],
|
||
'latin': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],
|
||
'tribal': [0.0, 0.5, 1.25, 1.75, 2.5, 3.0, 3.75],
|
||
'filtered': [0.5, 2.5],
|
||
'fading': [1.5],
|
||
'strip_down': [0.0],
|
||
'hint': [2.0],
|
||
},
|
||
'ride': {
|
||
'sparse': [0.0, 2.0],
|
||
'building': [0.0, 1.0, 2.0, 3.0],
|
||
'rising': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
|
||
'full': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'groove': [0.0, 0.25, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'energy': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5],
|
||
'filtered': [0.0, 2.0],
|
||
'fading': [0.0],
|
||
'minimal': [0.0],
|
||
'absent': [],
|
||
},
|
||
'top_loop': {
|
||
'minimal': [0.25, 1.25, 2.25, 3.25],
|
||
'energy': [0.0, 0.25, 0.5, 1.0, 1.25, 1.5, 2.0, 2.25, 2.5, 3.0, 3.25, 3.5],
|
||
'building': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],
|
||
'full': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'layered': [0.25, 0.5, 0.75, 1.25, 1.5, 1.75, 2.25, 2.5, 2.75, 3.25, 3.5, 3.75],
|
||
'filtered': [0.5, 1.5, 2.5, 3.5],
|
||
'fading': [0.5, 2.5],
|
||
'hint': [1.5, 3.5],
|
||
'absent': [],
|
||
},
|
||
'snare_fill': {
|
||
'rolling': [2.0, 2.125, 2.25, 2.375, 2.5, 2.625, 2.75, 2.875, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],
|
||
'tension': [3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],
|
||
'drop_hit': [0.0],
|
||
'fill': [3.0, 3.25, 3.5, 3.75],
|
||
'end_fill': [0.0, 0.25, 0.5, 0.75],
|
||
'absent': [],
|
||
},
|
||
'tom_fill': {
|
||
'rising': [3.0, 3.2, 3.4, 3.6, 3.8],
|
||
'fill': [3.0, 3.125, 3.25, 3.375, 3.5],
|
||
'drop_hit': [0.0],
|
||
'tension': [3.5, 3.625, 3.75, 3.875],
|
||
'end_fill': [0.0, 0.2, 0.4, 0.6],
|
||
'absent': [],
|
||
},
|
||
}
|
||
|
||
# Section-specific bass variants - EXPANDED
|
||
BASS_SECTION_VARIANTS = {
|
||
'intro': ['subtle', 'hint', 'foreshadow', 'ghost', 'minimal'],
|
||
'build': ['rising', 'tension', 'anticipate', 'building', 'pressure'],
|
||
'drop': ['full', 'punch', 'groove', 'deep', 'impact', 'energy', 'rolling'],
|
||
'break': ['sparse', 'minimal', 'atmos', 'filtered', 'foreshadow'],
|
||
'outro': ['fading', 'minimal', 'subtle', 'strip_down'],
|
||
}
|
||
|
||
# Expanded bass pattern templates (relative positions in 4-bar cycle)
|
||
BASS_PATTERN_BANKS = {
|
||
'anchor': {
|
||
'positions': [0.0, 1.0, 2.0, 3.0],
|
||
'durations': [0.5, 0.5, 0.5, 0.5],
|
||
'style': 'root_heavy'
|
||
},
|
||
'subtle': {
|
||
'positions': [0.0, 2.0],
|
||
'durations': [0.3, 0.3],
|
||
'style': 'minimal'
|
||
},
|
||
'hint': {
|
||
'positions': [0.0, 3.5],
|
||
'durations': [0.25, 0.25],
|
||
'style': 'foreshadow'
|
||
},
|
||
'foreshadow': {
|
||
'positions': [0.0, 1.0, 3.0, 3.5],
|
||
'durations': [0.4, 0.3, 0.4, 0.3],
|
||
'style': 'building'
|
||
},
|
||
'ghost': {
|
||
'positions': [0.5, 2.5],
|
||
'durations': [0.2, 0.2],
|
||
'style': 'minimal'
|
||
},
|
||
'rising': {
|
||
'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5, 0.4],
|
||
'style': 'ascending'
|
||
},
|
||
'tension': {
|
||
'positions': [0.0, 0.75, 1.5, 2.25, 3.0, 3.5],
|
||
'durations': [0.5, 0.25, 0.5, 0.25, 0.5, 0.3],
|
||
'style': 'syncopated'
|
||
},
|
||
'anticipate': {
|
||
'positions': [0.0, 1.0, 2.0, 2.75, 3.0, 3.25, 3.5],
|
||
'durations': [0.5, 0.5, 0.4, 0.2, 0.4, 0.2, 0.4],
|
||
'style': 'building'
|
||
},
|
||
'building': {
|
||
'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75],
|
||
'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.3, 0.2, 0.3, 0.2],
|
||
'style': 'ascending'
|
||
},
|
||
'pressure': {
|
||
'positions': [0.0, 0.25, 0.5, 0.75, 1.0, 1.5, 2.0, 2.5, 3.0, 3.25, 3.5, 3.75],
|
||
'durations': [0.3, 0.2, 0.3, 0.2, 0.4, 0.4, 0.4, 0.4, 0.3, 0.2, 0.3, 0.2],
|
||
'style': 'intense'
|
||
},
|
||
'full': {
|
||
'positions': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.5, 0.4, 0.5, 0.4, 0.5, 0.4, 0.5, 0.4],
|
||
'style': 'groove'
|
||
},
|
||
'punch': {
|
||
'positions': [0.0, 0.25, 1.0, 2.0, 3.0],
|
||
'durations': [0.6, 0.2, 0.5, 0.5, 0.5],
|
||
'style': 'punchy'
|
||
},
|
||
'groove': {
|
||
'positions': [0.0, 0.25, 0.75, 1.0, 1.75, 2.0, 2.75, 3.0, 3.5],
|
||
'durations': [0.4, 0.2, 0.3, 0.4, 0.3, 0.4, 0.3, 0.4, 0.3],
|
||
'style': 'syncopated'
|
||
},
|
||
'deep': {
|
||
'positions': [0.0, 1.0, 2.0, 3.0],
|
||
'durations': [0.8, 0.8, 0.8, 0.8],
|
||
'style': 'sub'
|
||
},
|
||
'impact': {
|
||
'positions': [0.0, 0.5, 1.5, 2.0, 3.0, 3.5],
|
||
'durations': [0.6, 0.4, 0.3, 0.5, 0.5, 0.4],
|
||
'style': 'punchy'
|
||
},
|
||
'energy': {
|
||
'positions': [0.0, 0.25, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.4, 0.25, 0.4, 0.5, 0.4, 0.5, 0.4, 0.5, 0.4],
|
||
'style': 'driving'
|
||
},
|
||
'rolling': {
|
||
'positions': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.25, 2.5, 2.75, 3.0, 3.25, 3.5, 3.75],
|
||
'durations': [0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15, 0.2, 0.15],
|
||
'style': 'rolling'
|
||
},
|
||
'sparse': {
|
||
'positions': [0.0, 2.0],
|
||
'durations': [0.4, 0.4],
|
||
'style': 'minimal'
|
||
},
|
||
'minimal': {
|
||
'positions': [0.0],
|
||
'durations': [0.3],
|
||
'style': 'hint'
|
||
},
|
||
'atmos': {
|
||
'positions': [0.0, 3.0],
|
||
'durations': [0.6, 0.4],
|
||
'style': 'atmospheric'
|
||
},
|
||
'filtered': {
|
||
'positions': [0.0, 1.5, 2.5],
|
||
'durations': [0.4, 0.3, 0.3],
|
||
'style': 'filtered'
|
||
},
|
||
'fading': {
|
||
'positions': [0.0, 2.0],
|
||
'durations': [0.5, 0.3],
|
||
'style': 'decay'
|
||
},
|
||
'strip_down': {
|
||
'positions': [0.0],
|
||
'durations': [0.25],
|
||
'style': 'minimal'
|
||
},
|
||
'bounce': {
|
||
'positions': [0.0, 0.5, 1.5, 2.0, 2.5, 3.5],
|
||
'durations': [0.4, 0.3, 0.4, 0.4, 0.3, 0.4],
|
||
'style': 'bouncy'
|
||
},
|
||
'syncopated': {
|
||
'positions': [0.25, 0.75, 1.25, 1.75, 2.25, 2.75, 3.25, 3.75],
|
||
'durations': [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2],
|
||
'style': 'offbeat'
|
||
},
|
||
}
|
||
|
||
# Pattern variant diversity memory - track used variants across generations
|
||
_pattern_variant_memory: Dict[str, Dict[str, int]] = {
|
||
'drum': {},
|
||
'bass': {},
|
||
'melodic': {},
|
||
}
|
||
|
||
def _get_pattern_variant_penalty(category: str, variant: str) -> float:
|
||
"""Get penalty for a pattern variant based on cross-generation usage."""
|
||
if variant in _pattern_variant_memory.get(category, {}):
|
||
count = _pattern_variant_memory[category].get(variant, 0)
|
||
return min(0.4, count * 0.08)
|
||
return 0.0
|
||
|
||
def _record_pattern_variant_usage(category: str, variant: str) -> None:
|
||
"""Record that a pattern variant was used."""
|
||
if category not in _pattern_variant_memory:
|
||
_pattern_variant_memory[category] = {}
|
||
_pattern_variant_memory[category][variant] = _pattern_variant_memory[category].get(variant, 0) + 1
|
||
|
||
def _decay_pattern_variant_memory() -> None:
|
||
"""Decay pattern variant memory to allow reuse after generations."""
|
||
for category in _pattern_variant_memory:
|
||
for variant in list(_pattern_variant_memory[category].keys()):
|
||
_pattern_variant_memory[category][variant] = max(0, _pattern_variant_memory[category][variant] - 1)
|
||
if _pattern_variant_memory[category][variant] <= 0:
|
||
del _pattern_variant_memory[category][variant]
|
||
|
||
def reset_pattern_variant_memory() -> None:
|
||
"""Reset all pattern variant memory."""
|
||
global _pattern_variant_memory
|
||
_pattern_variant_memory = {'drum': {}, 'bass': {}, 'melodic': {}}
|
||
|
||
# Expanded fill patterns for section transitions
|
||
FILL_PATTERNS = {
|
||
'drum_fill_4bar': {
|
||
'roles': ['snare', 'kick', 'hat'],
|
||
'pattern': {
|
||
'snare': [3.0, 3.25, 3.5, 3.75],
|
||
'kick': [3.5],
|
||
'hat': [3.0, 3.5]
|
||
},
|
||
'velocities': {'snare': 100, 'kick': 90, 'hat': 70}
|
||
},
|
||
'drum_fill_2bar': {
|
||
'roles': ['snare', 'hat'],
|
||
'pattern': {
|
||
'snare': [1.5, 1.75],
|
||
'hat': [1.5]
|
||
},
|
||
'velocities': {'snare': 95, 'hat': 65}
|
||
},
|
||
'snare_roll': {
|
||
'roles': ['snare'],
|
||
'pattern': {
|
||
'snare': [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25, 1.375, 1.5, 1.625, 1.75, 1.875]
|
||
},
|
||
'velocities': {'snare': 85}
|
||
},
|
||
'hat_open_build': {
|
||
'roles': ['hat_open'],
|
||
'pattern': {
|
||
'hat_open': [0.0, 0.5, 1.0, 1.5, 2.0, 2.25, 2.5, 2.75, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875]
|
||
},
|
||
'velocities': {'hat_open': 75}
|
||
},
|
||
'kick_drop': {
|
||
'roles': ['kick'],
|
||
'pattern': {
|
||
'kick': [0.0]
|
||
},
|
||
'velocities': {'kick': 127}
|
||
},
|
||
'crash_impact': {
|
||
'roles': ['crash'],
|
||
'pattern': {
|
||
'crash': [0.0]
|
||
},
|
||
'velocities': {'crash': 100}
|
||
},
|
||
'snare_roll_build': {
|
||
'roles': ['snare', 'hat'],
|
||
'pattern': {
|
||
'snare': [2.0, 2.25, 2.5, 2.75, 3.0, 3.125, 3.25, 3.375, 3.5, 3.625, 3.75, 3.875],
|
||
'hat': [2.0, 2.5, 3.0, 3.5]
|
||
},
|
||
'velocities': {'snare': 88, 'hat': 70}
|
||
},
|
||
'tom_build': {
|
||
'roles': ['tom_fill'],
|
||
'pattern': {
|
||
'tom_fill': [2.0, 2.2, 2.4, 2.6, 2.8, 3.0, 3.2, 3.4, 3.6, 3.8]
|
||
},
|
||
'velocities': {'tom_fill': 90}
|
||
},
|
||
'full_impact': {
|
||
'roles': ['kick', 'snare', 'crash'],
|
||
'pattern': {
|
||
'kick': [0.0],
|
||
'snare': [0.0, 0.25],
|
||
'crash': [0.0]
|
||
},
|
||
'velocities': {'kick': 127, 'snare': 110, 'crash': 105}
|
||
},
|
||
'hat_tension': {
|
||
'roles': ['hat_closed'],
|
||
'pattern': {
|
||
'hat_closed': [0.0, 0.125, 0.25, 0.375, 0.5, 0.625, 0.75, 0.875, 1.0, 1.125, 1.25, 1.375, 1.5, 1.625, 1.75, 1.875]
|
||
},
|
||
'velocities': {'hat_closed': 72}
|
||
},
|
||
'percussion_fill': {
|
||
'roles': ['perc'],
|
||
'pattern': {
|
||
'perc': [0.5, 0.75, 1.25, 1.5, 2.0, 2.5, 3.0, 3.5]
|
||
},
|
||
'velocities': {'perc': 78}
|
||
},
|
||
'minimal_drop': {
|
||
'roles': ['kick'],
|
||
'pattern': {
|
||
'kick': [0.0]
|
||
},
|
||
'velocities': {'kick': 120}
|
||
},
|
||
'build_tension': {
|
||
'roles': ['snare', 'hat_closed', 'kick'],
|
||
'pattern': {
|
||
'snare': [2.5, 2.75, 3.0, 3.25, 3.5, 3.75],
|
||
'hat_closed': [2.0, 2.5, 3.0, 3.5],
|
||
'kick': [0.0]
|
||
},
|
||
'velocities': {'snare': 92, 'hat_closed': 68, 'kick': 95}
|
||
},
|
||
'outro_fade': {
|
||
'roles': ['hat_closed', 'perc'],
|
||
'pattern': {
|
||
'hat_closed': [0.0, 0.5, 1.0],
|
||
'perc': [0.25, 0.75, 1.25]
|
||
},
|
||
'velocities': {'hat_closed': 80, 'perc': 70}
|
||
},
|
||
}
|
||
|
||
# Expanded transition events between sections
|
||
TRANSITION_EVENTS = {
|
||
('intro', 'build'): ['hat_tension', 'hat_open_build'],
|
||
('build', 'drop'): ['full_impact', 'crash_impact', 'kick_drop', 'snare_roll_build'],
|
||
('drop', 'break'): ['drum_fill_4bar', 'percussion_fill'],
|
||
('break', 'build'): ['hat_tension', 'hat_open_build'],
|
||
('break', 'drop'): ['crash_impact', 'kick_drop', 'full_impact'],
|
||
('drop', 'outro'): ['drum_fill_2bar', 'outro_fade'],
|
||
('outro', 'end'): ['minimal_drop'],
|
||
}
|
||
|
||
# Rules for preventing transition overcrowding
|
||
TRANSITION_DENSITY_RULES = {
|
||
# Max fills per section kind
|
||
'max_fills_by_section': {
|
||
'intro': 1, # Minimal fills in intro
|
||
'build': 3, # More fills for tension
|
||
'drop': 2, # Moderate fills
|
||
'break': 2, # Sparse
|
||
'outro': 1, # Minimal
|
||
},
|
||
|
||
# Events that should not stack together
|
||
'exclusive_events': [
|
||
{'crash_impact', 'kick_drop'}, # Don't stack impact events
|
||
{'drum_fill_4bar', 'snare_roll'}, # Choose one drum fill
|
||
],
|
||
|
||
# Minimum distance between same-type fills (in beats)
|
||
'min_distance_same_type': {
|
||
'crash_impact': 8.0,
|
||
'kick_drop': 16.0,
|
||
'snare_roll': 4.0,
|
||
}
|
||
}
|
||
|
||
# Section-specific melodic variants - EXPANDED
|
||
MELODIC_SECTION_VARIANTS = {
|
||
'intro': ['subtle', 'foreshadow', 'atmospheric', 'ghost', 'hint'],
|
||
'build': ['rising', 'tension', 'anticipate', 'building', 'energy'],
|
||
'drop': ['hook', 'anthem', 'full', 'punchy', 'impact', 'driving'],
|
||
'break': ['sparse', 'minimal', 'ethereal', 'filtered', 'atmospheric'],
|
||
'outro': ['fading', 'echo', 'minimal', 'strip_down', 'decay'],
|
||
}
|
||
|
||
# Expanded melodic pattern templates
|
||
MELODIC_PATTERN_BANKS = {
|
||
'motif': {
|
||
'intervals': [0, 4, 7, 0],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5],
|
||
'durations': [0.4, 0.3, 0.4, 0.3],
|
||
'style': 'repeating'
|
||
},
|
||
'subtle': {
|
||
'intervals': [0, 0],
|
||
'rhythm': [0.0, 2.0],
|
||
'durations': [0.3, 0.3],
|
||
'style': 'minimal'
|
||
},
|
||
'foreshadow': {
|
||
'intervals': [0, 4, 0],
|
||
'rhythm': [0.0, 1.0, 3.5],
|
||
'durations': [0.4, 0.3, 0.5],
|
||
'style': 'hint'
|
||
},
|
||
'atmospheric': {
|
||
'intervals': [0, 2, 4, 5, 7],
|
||
'rhythm': [0.0, 0.8, 1.6, 2.4, 3.2],
|
||
'durations': [0.8, 0.7, 0.6, 0.5, 0.4],
|
||
'style': 'pad'
|
||
},
|
||
'ghost': {
|
||
'intervals': [0, 7],
|
||
'rhythm': [0.5, 2.5],
|
||
'durations': [0.2, 0.2],
|
||
'style': 'minimal'
|
||
},
|
||
'hint': {
|
||
'intervals': [0, 5],
|
||
'rhythm': [0.0, 3.0],
|
||
'durations': [0.25, 0.25],
|
||
'style': 'minimal'
|
||
},
|
||
'rising': {
|
||
'intervals': [0, 2, 4, 5, 7, 9, 11, 12],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.4, 0.35, 0.4, 0.35, 0.4, 0.35, 0.5, 0.4],
|
||
'style': 'ascending'
|
||
},
|
||
'tension': {
|
||
'intervals': [0, 1, 0, 1, 2, 1, 0],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
|
||
'durations': [0.3, 0.2, 0.3, 0.2, 0.3, 0.2, 0.5],
|
||
'style': 'chromatic'
|
||
},
|
||
'anticipate': {
|
||
'intervals': [0, 4, 7, 9, 12],
|
||
'rhythm': [0.0, 1.0, 2.0, 3.0, 3.75],
|
||
'durations': [0.5, 0.4, 0.5, 0.3, 0.5],
|
||
'style': 'buildup'
|
||
},
|
||
'building': {
|
||
'intervals': [0, 2, 4, 5, 7, 9, 11],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.75, 3.5],
|
||
'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5],
|
||
'style': 'ascending'
|
||
},
|
||
'energy': {
|
||
'intervals': [0, 4, 7, 9, 12, 14],
|
||
'rhythm': [0.0, 0.25, 0.75, 1.25, 2.0, 2.75],
|
||
'durations': [0.3, 0.25, 0.3, 0.25, 0.4, 0.5],
|
||
'style': 'driving'
|
||
},
|
||
'hook': {
|
||
'intervals': [0, 4, 7, 4, 0, 4, 7, 12],
|
||
'rhythm': [0.0, 0.25, 0.5, 0.75, 1.0, 1.25, 1.5, 1.75],
|
||
'durations': [0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.2, 0.3],
|
||
'style': 'catchy'
|
||
},
|
||
'anthem': {
|
||
'intervals': [0, 4, 7, 12, 11, 7, 4, 0],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.4, 0.4, 0.4, 0.5, 0.4, 0.4, 0.4, 0.5],
|
||
'style': 'big'
|
||
},
|
||
'full': {
|
||
'intervals': [0, 4, 7, 5, 4, 2, 0],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0],
|
||
'durations': [0.4, 0.3, 0.4, 0.3, 0.4, 0.3, 0.5],
|
||
'style': 'melodic'
|
||
},
|
||
'punchy': {
|
||
'intervals': [0, 7, 0, 12],
|
||
'rhythm': [0.0, 0.25, 0.5, 0.75],
|
||
'durations': [0.15, 0.15, 0.15, 0.2],
|
||
'style': 'staccato'
|
||
},
|
||
'impact': {
|
||
'intervals': [0, 5, 7, 12, 7, 5],
|
||
'rhythm': [0.0, 0.5, 0.75, 1.5, 2.25, 3.0],
|
||
'durations': [0.4, 0.25, 0.3, 0.5, 0.3, 0.4],
|
||
'style': 'driving'
|
||
},
|
||
'driving': {
|
||
'intervals': [0, 4, 7, 4, 0, 4, 5, 7],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5, 3.0, 3.5],
|
||
'durations': [0.35, 0.35, 0.35, 0.35, 0.35, 0.35, 0.35, 0.4],
|
||
'style': 'repeating'
|
||
},
|
||
'sparse': {
|
||
'intervals': [0, 7],
|
||
'rhythm': [0.0, 2.0],
|
||
'durations': [0.4, 0.4],
|
||
'style': 'minimal'
|
||
},
|
||
'minimal': {
|
||
'intervals': [0],
|
||
'rhythm': [0.0],
|
||
'durations': [0.3],
|
||
'style': 'single'
|
||
},
|
||
'ethereal': {
|
||
'intervals': [0, 7, 12, 7],
|
||
'rhythm': [0.0, 1.5, 2.5, 3.5],
|
||
'durations': [1.0, 0.8, 1.0, 0.8],
|
||
'style': 'pad'
|
||
},
|
||
'filtered': {
|
||
'intervals': [0, 4, 7, 5],
|
||
'rhythm': [0.0, 1.0, 2.0, 3.0],
|
||
'durations': [0.5, 0.4, 0.5, 0.4],
|
||
'style': 'filtered'
|
||
},
|
||
'fading': {
|
||
'intervals': [0, 4, 0],
|
||
'rhythm': [0.0, 1.0, 2.0],
|
||
'durations': [0.5, 0.4, 0.3],
|
||
'style': 'decay'
|
||
},
|
||
'echo': {
|
||
'intervals': [0, 0, 0],
|
||
'rhythm': [0.0, 0.5, 1.0],
|
||
'durations': [0.3, 0.25, 0.2],
|
||
'style': 'repeat'
|
||
},
|
||
'response': {
|
||
'intervals': [7, 4, 0],
|
||
'rhythm': [0.5, 1.5, 2.5],
|
||
'durations': [0.3, 0.3, 0.4],
|
||
'style': 'call_response'
|
||
},
|
||
'lift': {
|
||
'intervals': [0, 4, 7, 12, 14, 16],
|
||
'rhythm': [0.0, 0.5, 1.0, 1.5, 2.0, 2.5],
|
||
'durations': [0.3, 0.3, 0.3, 0.4, 0.3, 0.4],
|
||
'style': 'ascending'
|
||
},
|
||
'strip_down': {
|
||
'intervals': [0],
|
||
'rhythm': [0.0],
|
||
'durations': [0.25],
|
||
'style': 'minimal'
|
||
},
|
||
'decay': {
|
||
'intervals': [0, 7, 5, 3],
|
||
'rhythm': [0.0, 1.0, 2.0, 3.0],
|
||
'durations': [0.5, 0.4, 0.3, 0.2],
|
||
'style': 'descending'
|
||
},
|
||
'call_response': {
|
||
'intervals': [0, 4, 7, 0, 7, 4],
|
||
'rhythm': [0.0, 0.25, 0.5, 1.5, 2.0, 2.5],
|
||
'durations': [0.25, 0.2, 0.3, 0.35, 0.25, 0.3],
|
||
'style': 'call_response'
|
||
},
|
||
}
|
||
|
||
# =============================================================================
|
||
# MASTER CHAIN AUTOMATION TARGETS
|
||
# =============================================================================
|
||
|
||
|
||
@dataclass
|
||
class StyleConfig:
|
||
"""Configuración de estilo musical"""
|
||
genre: str
|
||
bpm: float
|
||
key: str
|
||
scale: str
|
||
density: str # minimal, normal, busy
|
||
complexity: str # simple, moderate, complex
|
||
|
||
|
||
|
||
|
||
class HumanFeelEngine:
|
||
"""
|
||
T040-T050: Engine de humanizacion y dinamica.
|
||
Aplica variaciones de timing, velocity y groove a patrones MIDI.
|
||
"""
|
||
|
||
def __init__(self, seed: int = 42):
|
||
self.rng = random.Random(seed)
|
||
self._groove_templates = {
|
||
'straight': {'swing': 0.0, 'humanize': 0.0},
|
||
'shuffle': {'swing': 0.33, 'humanize': 0.02},
|
||
'triplet': {'swing': 0.66, 'humanize': 0.03},
|
||
'latin': {'swing': 0.25, 'humanize': 0.04},
|
||
}
|
||
|
||
def apply_timing_variation(self, notes: List[Dict], amount_ms: float = 5.0) -> List[Dict]:
|
||
"""T040: Micro-offsets de timing (-5ms a +5ms)."""
|
||
result = []
|
||
for note in notes:
|
||
offset = self.rng.uniform(-amount_ms, amount_ms) / 1000.0 # Convert to seconds
|
||
new_note = dict(note)
|
||
new_note['start'] = note.get('start', 0) + offset
|
||
result.append(new_note)
|
||
return result
|
||
|
||
def apply_velocity_humanize(self, notes: List[Dict], variance: float = 0.05) -> List[Dict]:
|
||
"""T041: Humanizacion de velocity (+-5% variacion)."""
|
||
result = []
|
||
for note in notes:
|
||
vel = note.get('velocity', 100)
|
||
variation = self.rng.uniform(-variance, variance)
|
||
new_vel = int(vel * (1 + variation))
|
||
new_vel = max(1, min(127, new_vel)) # Clamp to MIDI range
|
||
new_note = dict(note)
|
||
new_note['velocity'] = new_vel
|
||
result.append(new_note)
|
||
return result
|
||
|
||
def apply_note_skip_probability(self, notes: List[Dict], prob: float = 0.02) -> List[Dict]:
|
||
"""T042: Probabilidad de skip nota (2% ghost notes)."""
|
||
result = []
|
||
for note in notes:
|
||
if self.rng.random() > prob: # Keep note with probability (1-prob)
|
||
result.append(note)
|
||
return result
|
||
|
||
def apply_groove(self, notes: List[Dict], style: str = 'shuffle', amount: float = 0.5) -> List[Dict]:
|
||
"""T044-T046: Aplica groove template."""
|
||
template = self._groove_templates.get(style, self._groove_templates['straight'])
|
||
swing = template['swing'] * amount
|
||
|
||
result = []
|
||
for note in notes:
|
||
start = note.get('start', 0)
|
||
# Apply swing to off-beat notes
|
||
beat_pos = start % 1.0 # Position within beat
|
||
if 0.4 < beat_pos < 0.6: # Off-beat
|
||
delay = swing * 0.1 # Max 100ms delay
|
||
new_note = dict(note)
|
||
new_note['start'] = start + delay
|
||
result.append(new_note)
|
||
else:
|
||
result.append(note)
|
||
return result
|
||
|
||
def apply_section_dynamics(self, notes: List[Dict], section: str) -> List[Dict]:
|
||
"""T047-T050: Dinamica por seccion (intro 70%, drop 100%, etc)."""
|
||
section_scales = {
|
||
'intro': 0.70,
|
||
'build': 0.85,
|
||
'drop': 1.00,
|
||
'break': 0.75,
|
||
'outro': 0.60,
|
||
}
|
||
scale = section_scales.get(section.lower(), 1.0)
|
||
|
||
result = []
|
||
for note in notes:
|
||
vel = note.get('velocity', 100)
|
||
new_vel = int(vel * scale)
|
||
new_vel = max(1, min(127, new_vel))
|
||
new_note = dict(note)
|
||
new_note['velocity'] = new_vel
|
||
result.append(new_note)
|
||
return result
|
||
|
||
def process_notes(self, notes: List[Dict], section: str = 'drop',
|
||
humanize: bool = True, groove_style: str = 'shuffle') -> List[Dict]:
|
||
"""Procesamiento completo con todos los efectos."""
|
||
result = list(notes)
|
||
if humanize:
|
||
result = self.apply_timing_variation(result)
|
||
result = self.apply_velocity_humanize(result)
|
||
result = self.apply_note_skip_probability(result)
|
||
result = self.apply_groove(result, groove_style)
|
||
result = self.apply_section_dynamics(result, section)
|
||
return result
|
||
|
||
class SongGenerator:
|
||
"""Generador de configuraciones y patrones musicales"""
|
||
|
||
def __init__(self):
|
||
self.logger = logging.getLogger("SongGenerator")
|
||
self._current_generation_profile = {
|
||
'name': 'default',
|
||
'seed': 0,
|
||
'drum_tightness': 1.0,
|
||
'bass_motion': 'locked',
|
||
'melodic_motion': 'restrained',
|
||
'pan_width': 0.12,
|
||
'fx_bias': 1.0,
|
||
}
|
||
# Track style adjustments and calibrated volumes for this generation
|
||
self._style_adjustments_applied = []
|
||
self._calibrated_bus_volumes = {}
|
||
# Tracking for ROLE_GAIN_CALIBRATION overrides
|
||
self._gain_calibration_overrides_count = 0
|
||
self._peak_reductions_count = 0
|
||
self._master_profile_used = 'default'
|
||
|
||
# =========================================================================
|
||
# UTILIDADES MUSICALES
|
||
# =========================================================================
|
||
|
||
def note_name_to_midi(self, note_name: str, octave: int = 3) -> int:
|
||
"""Convierte nombre de nota a número MIDI"""
|
||
note_name = note_name.replace('b', '#').replace('Db', 'C#').replace('Eb', 'D#')
|
||
note_name = note_name.replace('Gb', 'F#').replace('Ab', 'G#').replace('Bb', 'A#')
|
||
|
||
try:
|
||
note_idx = NOTE_NAMES.index(note_name.upper())
|
||
return (octave + 1) * 12 + note_idx
|
||
except ValueError:
|
||
return 60 # Default C4
|
||
|
||
def midi_to_note_name(self, midi_note: int) -> tuple:
|
||
"""Convierte MIDI a (nota, octava)"""
|
||
octave = (midi_note // 12) - 1
|
||
note_name = NOTE_NAMES[midi_note % 12]
|
||
return note_name, octave
|
||
|
||
def get_scale_notes(self, root_note: Union[int, str], scale_name: str = 'minor') -> List[int]:
|
||
"""Obtiene las notas de una escala"""
|
||
if isinstance(root_note, str):
|
||
root_midi = self.note_name_to_midi(root_note)
|
||
else:
|
||
root_midi = root_note
|
||
|
||
scale_intervals = SCALES.get(scale_name, SCALES['minor'])
|
||
return [root_midi + interval for interval in scale_intervals]
|
||
|
||
def quantize_to_scale(self, note: int, scale_notes: List[int]) -> int:
|
||
"""Cuantiza una nota a la escala más cercana"""
|
||
if note in scale_notes:
|
||
return note
|
||
return min(scale_notes, key=lambda x: abs(x - note))
|
||
|
||
# =========================================================================
|
||
# GENERACIÓN DE CONFIGURACIONES
|
||
# =========================================================================
|
||
|
||
def _make_note(self, pitch: int, start: float, duration: float, velocity: int) -> Dict[str, Any]:
|
||
return {
|
||
'pitch': max(0, min(127, int(pitch))),
|
||
'start': round(float(start), 3),
|
||
'duration': round(max(0.05, float(duration)), 3),
|
||
'velocity': max(1, min(127, int(velocity))),
|
||
}
|
||
|
||
def _repeat_pattern(self, pattern: List[Dict[str, Any]], total_length: float, pattern_length: float = 4.0) -> List[Dict[str, Any]]:
|
||
if not pattern or total_length <= 0 or pattern_length <= 0:
|
||
return []
|
||
|
||
notes = []
|
||
repeats = max(1, int(round(total_length / pattern_length)))
|
||
for repeat_index in range(repeats):
|
||
offset = repeat_index * pattern_length
|
||
for note in pattern:
|
||
start = float(note['start']) + offset
|
||
if start >= total_length:
|
||
continue
|
||
duration = min(float(note['duration']), total_length - start)
|
||
notes.append(self._make_note(note['pitch'], start, duration, note['velocity']))
|
||
return notes
|
||
|
||
def _section_rng(self, section: Dict[str, Any], role: str, salt: int = 0) -> random.Random:
|
||
base_seed = int(self._current_generation_profile.get('seed', 0))
|
||
section_index = int(section.get('index', 0))
|
||
role_fingerprint = sum((index + 1) * ord(char) for index, char in enumerate(str(role)))
|
||
return random.Random(base_seed + (section_index * 1009) + (role_fingerprint * 17) + (salt * 7919))
|
||
|
||
def _clamp_pan(self, value: float) -> float:
|
||
return round(max(-1.0, min(1.0, float(value))), 3)
|
||
|
||
def _clamp_unit(self, value: float) -> float:
|
||
return round(max(0.0, min(1.0, float(value))), 3)
|
||
|
||
def _apply_swing(self, notes: List[Dict[str, Any]], amount: float, section_length: float) -> List[Dict[str, Any]]:
|
||
if not notes or abs(amount) < 0.001:
|
||
return notes
|
||
|
||
swung = []
|
||
for note in notes:
|
||
start = float(note['start'])
|
||
fractional = round(start % 1.0, 3)
|
||
if 0.001 < fractional < 0.999:
|
||
shift = amount if fractional >= 0.5 else (amount * -0.45)
|
||
start = min(max(0.0, start + shift), max(0.0, section_length - 0.05))
|
||
swung.append(self._make_note(note['pitch'], start, note['duration'], note['velocity']))
|
||
swung.sort(key=lambda item: (item['start'], item['pitch']))
|
||
return swung
|
||
|
||
def _apply_density_mask(self, notes: List[Dict[str, Any]], section: Dict[str, Any], role: str,
|
||
keep_probability: float) -> List[Dict[str, Any]]:
|
||
if not notes or keep_probability >= 0.995:
|
||
return notes
|
||
|
||
rng = self._section_rng(section, role, salt=3)
|
||
filtered = []
|
||
for note in notes:
|
||
start = float(note['start'])
|
||
if abs(start % 1.0) < 0.001:
|
||
filtered.append(note)
|
||
continue
|
||
if rng.random() <= keep_probability:
|
||
filtered.append(note)
|
||
return filtered or notes[:1]
|
||
|
||
def _build_arrangement_profile(self, genre: str, style: str, variant_seed: int) -> Dict[str, Any]:
|
||
style_text = "{} {}".format(genre, style).lower()
|
||
candidates = [profile for profile in ARRANGEMENT_PROFILES if genre in set(profile.get('genres', ()))]
|
||
|
||
if 'latin' in style_text:
|
||
candidates = [profile for profile in ARRANGEMENT_PROFILES if profile['name'] in ['swing', 'jackin']] or candidates
|
||
elif 'industrial' in style_text:
|
||
candidates = [profile for profile in ARRANGEMENT_PROFILES if profile['name'] in ['warehouse', 'festival']] or candidates
|
||
|
||
if not candidates:
|
||
candidates = list(ARRANGEMENT_PROFILES)
|
||
|
||
rng = random.Random(int(variant_seed) + 41)
|
||
selected = dict(rng.choice(candidates))
|
||
selected['seed'] = int(variant_seed)
|
||
return selected
|
||
|
||
def _extend_parallel_sends(self, role: str, sends: Dict[str, Any]) -> Dict[str, Any]:
|
||
resolved = dict(sends or {})
|
||
if role in ['kick', 'clap', 'hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'snare_fill', 'tom_fill']:
|
||
resolved.setdefault('glue', 0.1)
|
||
resolved.setdefault('heat', 0.05)
|
||
elif role in ['sub_bass', 'bass', 'stab']:
|
||
resolved.setdefault('glue', 0.08)
|
||
resolved.setdefault('heat', 0.08)
|
||
elif role in ['chords', 'pad', 'pluck', 'arp', 'lead', 'counter', 'vocal']:
|
||
resolved.setdefault('glue', 0.04)
|
||
elif role in ['reverse_fx', 'riser', 'impact', 'atmos', 'drone', 'crash']:
|
||
resolved.setdefault('glue', 0.03)
|
||
return resolved
|
||
|
||
def _resolve_bus_for_role(self, role: str) -> Optional[str]:
|
||
return ROLE_BUS_ASSIGNMENTS.get(str(role or '').strip().lower(), 'music')
|
||
|
||
def _get_section_variation(self, role: str, section_kind: str, genre: str = "") -> Dict[str, Any]:
|
||
"""
|
||
Obtiene configuracion de variacion para un rol y seccion.
|
||
|
||
Retorna dict con:
|
||
- use: bool - si el rol debe usarse en esta seccion
|
||
- sparse: bool - si usar variante sparse
|
||
- full: bool - si usar variante completa
|
||
- intensity: float - intensidad de 0 a 1
|
||
- etc.
|
||
"""
|
||
# TODO-008: Usar variantes especificas de reggaeton si aplica
|
||
if genre.lower() == "reggaeton" and role in REGGAETON_SECTION_VARIANTS:
|
||
reggaeton_config = REGGAETON_SECTION_VARIANTS[role]
|
||
return reggaeton_config.get(section_kind.lower(), {"use": True, "intensity": 1.0})
|
||
|
||
if role not in SECTION_VARIATION_CONFIG:
|
||
return {"use": True, "intensity": 1.0}
|
||
|
||
role_config = SECTION_VARIATION_CONFIG[role]
|
||
return role_config.get(section_kind.lower(), {"use": True, "intensity": 1.0})
|
||
|
||
def _should_vary_role_in_section(self, role: str, section_kind: str, genre: str = "") -> bool:
|
||
"""Determina si un rol debe variar en una seccion dada."""
|
||
if role not in SECTION_VARIATION_CONFIG and role not in REGGAETON_SECTION_VARIANTS:
|
||
return False
|
||
|
||
config = self._get_section_variation(role, section_kind, genre)
|
||
|
||
# Si tiene clave 'use' explÃcita
|
||
if 'use' in config:
|
||
return config['use']
|
||
|
||
# Si tiene variantes especÃficas
|
||
return any(k in config for k in ['sparse', 'full', 'building', 'fading'])
|
||
|
||
def _build_mix_bus_blueprint(
|
||
self,
|
||
profile: Dict[str, Any],
|
||
genre: str,
|
||
style: str,
|
||
reference_resolution: Optional[Dict[str, Any]] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
style_text = f"{genre} {style}".lower()
|
||
profile_name = str(profile.get('name', 'default')).lower()
|
||
reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()
|
||
|
||
buses = [
|
||
{
|
||
'key': 'drums',
|
||
'name': 'DRUM BUS',
|
||
'color': BUS_TRACK_COLORS['drums'],
|
||
'volume': 0.86,
|
||
'pan': 0.0,
|
||
'monitoring': 'in',
|
||
'fx_chain': [
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -16.5}},
|
||
{'device': 'Saturator', 'parameters': {'Drive': 1.2}},
|
||
{'device': 'Utility', 'parameters': {'Gain': 0.2}},
|
||
{'device': 'Limiter', 'parameters': {'Gain': 0.3}},
|
||
],
|
||
},
|
||
{
|
||
'key': 'bass',
|
||
'name': 'BASS BUS',
|
||
'color': BUS_TRACK_COLORS['bass'],
|
||
'volume': 0.8,
|
||
'pan': 0.0,
|
||
'monitoring': 'in',
|
||
'fx_chain': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 1.3}},
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -18.0}},
|
||
{'device': 'Utility', 'parameters': {'Stereo Width': 0.0}},
|
||
{'device': 'Utility', 'parameters': {'Gain': 0.2}},
|
||
],
|
||
},
|
||
{
|
||
'key': 'music',
|
||
'name': 'MUSIC BUS',
|
||
'color': BUS_TRACK_COLORS['music'],
|
||
'volume': 0.8,
|
||
'pan': 0.0,
|
||
'monitoring': 'in',
|
||
'fx_chain': [
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -21.0}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 12800.0, 'Dry/Wet': 0.05}},
|
||
{'device': 'Utility', 'parameters': {'Stereo Width': 1.12}},
|
||
{'device': 'Utility', 'parameters': {'Gain': 0.2}},
|
||
],
|
||
},
|
||
{
|
||
'key': 'vocal',
|
||
'name': 'VOCAL BUS',
|
||
'color': BUS_TRACK_COLORS['vocal'],
|
||
'volume': 0.82,
|
||
'pan': 0.0,
|
||
'monitoring': 'in',
|
||
'fx_chain': [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 0.05}},
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -18.0}},
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.05}},
|
||
{'device': 'Utility', 'parameters': {'Gain': 0.2}},
|
||
],
|
||
},
|
||
{
|
||
'key': 'fx',
|
||
'name': 'FX BUS',
|
||
'color': BUS_TRACK_COLORS['fx'],
|
||
'volume': 0.76,
|
||
'pan': 0.0,
|
||
'monitoring': 'in',
|
||
'fx_chain': [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 10200.0, 'Dry/Wet': 0.1}},
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}},
|
||
{'device': 'Utility', 'parameters': {'Gain': -0.2}},
|
||
{'device': 'Limiter', 'parameters': {'Gain': 0.0}},
|
||
],
|
||
},
|
||
]
|
||
|
||
# =========================================================================
|
||
# Apply BUS_GAIN_CALIBRATION as safe baseline BEFORE profile overrides
|
||
# =========================================================================
|
||
self._style_adjustments_applied = []
|
||
self._calibrated_bus_volumes = {}
|
||
|
||
def find_device_in_chain(fx_chain, device_type):
|
||
for device in fx_chain:
|
||
if device.get('device') == device_type:
|
||
return device
|
||
return None
|
||
|
||
for bus in buses:
|
||
bus_key = bus.get('key', '')
|
||
if bus_key not in BUS_GAIN_CALIBRATION:
|
||
continue
|
||
|
||
calibration = BUS_GAIN_CALIBRATION[bus_key]
|
||
|
||
if 'volume' in calibration:
|
||
bus['volume'] = calibration['volume']
|
||
|
||
fx_chain = bus.get('fx_chain', [])
|
||
|
||
if 'compressor_threshold' in calibration:
|
||
compressor = find_device_in_chain(fx_chain, 'Compressor')
|
||
if compressor:
|
||
compressor['parameters']['Threshold'] = calibration['compressor_threshold']
|
||
|
||
if 'saturator_drive' in calibration:
|
||
saturator = find_device_in_chain(fx_chain, 'Saturator')
|
||
if saturator:
|
||
saturator['parameters']['Drive'] = calibration['saturator_drive']
|
||
|
||
if 'limiter_gain' in calibration:
|
||
limiter = find_device_in_chain(fx_chain, 'Limiter')
|
||
if limiter:
|
||
limiter['parameters']['Gain'] = calibration['limiter_gain']
|
||
|
||
if 'utility_gain' in calibration:
|
||
for device in fx_chain:
|
||
if device.get('device') == 'Utility':
|
||
if 'Gain' in device.get('parameters', {}):
|
||
device['parameters']['Gain'] = calibration['utility_gain']
|
||
break
|
||
elif 'Stereo Width' not in device.get('parameters', {}):
|
||
device['parameters']['Gain'] = calibration['utility_gain']
|
||
break
|
||
|
||
# =========================================================================
|
||
# Profile-specific overrides ON TOP of calibrated baselines
|
||
# =========================================================================
|
||
if profile_name == 'warehouse':
|
||
buses[0]['name'] = 'DRUM BUNKER'
|
||
buses[0]['fx_chain'][1]['parameters']['Drive'] = 3.1
|
||
buses[1]['name'] = 'LOW END BUS'
|
||
buses[1]['fx_chain'][0]['parameters']['Drive'] = 4.0
|
||
buses[2]['fx_chain'][1]['parameters']['Frequency'] = 11200.0
|
||
elif profile_name == 'festival':
|
||
buses[2]['name'] = 'MUSIC WIDE'
|
||
buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.14
|
||
buses[3]['name'] = 'VOCAL TAIL'
|
||
buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08
|
||
buses[4]['name'] = 'FX WASH'
|
||
buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.14
|
||
elif profile_name == 'swing':
|
||
buses[0]['name'] = 'DRUM POCKET'
|
||
buses[0]['fx_chain'][0]['parameters']['Threshold'] = -13.5
|
||
buses[3]['name'] = 'VOCAL SLAP'
|
||
buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.12
|
||
elif profile_name == 'jackin':
|
||
buses[0]['name'] = 'DRUM CLUB'
|
||
buses[2]['name'] = 'MUSIC JACK'
|
||
buses[3]['name'] = 'VOX CLUB'
|
||
buses[4]['name'] = 'FX JAM'
|
||
elif profile_name == 'tech-house-club':
|
||
# Club-oriented tech-house with punchy drums and latin vocal treatment
|
||
buses[0]['name'] = 'DRUM CLUB'
|
||
buses[0]['volume'] = 0.95
|
||
buses[0]['fx_chain'][0]['parameters']['Threshold'] = -15.5
|
||
buses[0]['fx_chain'][1]['parameters']['Drive'] = 2.2
|
||
buses[1]['name'] = 'BASS TUBE'
|
||
buses[1]['volume'] = 0.95
|
||
buses[1]['fx_chain'][0]['parameters']['Drive'] = 2.5
|
||
buses[1]['fx_chain'][1]['parameters']['Threshold'] = -17.0
|
||
buses[2]['name'] = 'MUSIC JACK'
|
||
buses[2]['volume'] = 0.95
|
||
buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.16
|
||
buses[3]['name'] = 'VOCAL LATIN BUS'
|
||
buses[3]['volume'] = 0.95
|
||
buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.10
|
||
buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08
|
||
buses[4]['name'] = 'FX JAM'
|
||
buses[4]['volume'] = 0.95
|
||
buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.14
|
||
elif profile_name == 'tech-house-deep':
|
||
# Minimal deep tech-house with subtle processing
|
||
buses[0]['name'] = 'DRUM DEEP'
|
||
buses[0]['volume'] = 0.95
|
||
buses[0]['fx_chain'][0]['parameters']['Threshold'] = -18.0
|
||
buses[0]['fx_chain'][1]['parameters']['Drive'] = 0.8
|
||
buses[1]['name'] = 'SUB DEEP'
|
||
buses[1]['volume'] = 0.95
|
||
buses[1]['fx_chain'][0]['parameters']['Drive'] = 1.0
|
||
buses[1]['fx_chain'][1]['parameters']['Threshold'] = -20.0
|
||
buses[2]['name'] = 'ATMOS DEEP'
|
||
buses[2]['volume'] = 0.95
|
||
buses[2]['fx_chain'][0]['parameters']['Threshold'] = -24.0
|
||
buses[2]['fx_chain'][1]['parameters']['Frequency'] = 10200.0
|
||
buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.08
|
||
buses[3]['name'] = 'VOX DEEP'
|
||
buses[3]['volume'] = 0.95
|
||
buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.04
|
||
buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.06
|
||
buses[4]['name'] = 'FX DEEP'
|
||
buses[4]['volume'] = 0.95
|
||
buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.08
|
||
elif profile_name == 'tech-house-funky':
|
||
# Groovy tech-house with wide stereo and bouncy feel
|
||
buses[0]['name'] = 'DRUM GROOVE'
|
||
buses[0]['volume'] = 0.95
|
||
buses[0]['fx_chain'][0]['parameters']['Threshold'] = -14.5
|
||
buses[0]['fx_chain'][1]['parameters']['Drive'] = 1.8
|
||
buses[1]['name'] = 'BASS FUNK'
|
||
buses[1]['volume'] = 0.95
|
||
buses[1]['fx_chain'][0]['parameters']['Drive'] = 2.0
|
||
buses[1]['fx_chain'][1]['parameters']['Threshold'] = -16.5
|
||
buses[2]['name'] = 'MUSIC GROOVE'
|
||
buses[2]['volume'] = 0.95
|
||
buses[2]['fx_chain'][0]['parameters']['Threshold'] = -20.0
|
||
buses[2]['fx_chain'][2]['parameters']['Stereo Width'] = 1.20
|
||
buses[3]['name'] = 'VOCAL FUNK'
|
||
buses[3]['volume'] = 0.95
|
||
buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.12
|
||
buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.10
|
||
buses[4]['name'] = 'FX SWING'
|
||
buses[4]['volume'] = 0.95
|
||
buses[4]['fx_chain'][1]['parameters']['Dry/Wet'] = 0.16
|
||
|
||
if 'industrial' in style_text:
|
||
buses[0]['fx_chain'][1]['parameters']['Drive'] = max(
|
||
3.4,
|
||
float(buses[0]['fx_chain'][1]['parameters'].get('Drive', 2.2)),
|
||
)
|
||
buses[1]['fx_chain'][0]['parameters']['Drive'] = max(
|
||
4.2,
|
||
float(buses[1]['fx_chain'][0]['parameters'].get('Drive', 3.2)),
|
||
)
|
||
if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃmica', 'quimica']):
|
||
buses[3]['name'] = 'VOCAL LATIN BUS'
|
||
buses[3]['fx_chain'][0]['parameters']['Dry/Wet'] = 0.14
|
||
buses[3]['fx_chain'][2]['parameters']['Dry/Wet'] = 0.08
|
||
buses[0]['fx_chain'][0]['parameters']['Threshold'] = -14.0
|
||
|
||
# =========================================================================
|
||
# Apply STYLE_GAIN_ADJUSTMENTS as multipliers AFTER profile overrides
|
||
# =========================================================================
|
||
for style_key, adjustments in STYLE_GAIN_ADJUSTMENTS.items():
|
||
if style_key.lower() in style_text:
|
||
self._style_adjustments_applied.append(style_key)
|
||
|
||
# Apply bus volume factors
|
||
if 'drums_bus_volume_factor' in adjustments:
|
||
for bus in buses:
|
||
if bus.get('key') == 'drums':
|
||
bus['volume'] = bus.get('volume', 0.8) * adjustments['drums_bus_volume_factor']
|
||
|
||
if 'bass_bus_volume_factor' in adjustments:
|
||
for bus in buses:
|
||
if bus.get('key') == 'bass':
|
||
bus['volume'] = bus.get('volume', 0.8) * adjustments['bass_bus_volume_factor']
|
||
|
||
if 'vocal_bus_volume_factor' in adjustments:
|
||
for bus in buses:
|
||
if bus.get('key') == 'vocal':
|
||
bus['volume'] = bus.get('volume', 0.8) * adjustments['vocal_bus_volume_factor']
|
||
|
||
if 'music_bus_volume_factor' in adjustments:
|
||
for bus in buses:
|
||
if bus.get('key') == 'music':
|
||
bus['volume'] = bus.get('volume', 0.8) * adjustments['music_bus_volume_factor']
|
||
|
||
if 'fx_bus_volume_factor' in adjustments:
|
||
for bus in buses:
|
||
if bus.get('key') == 'fx':
|
||
bus['volume'] = bus.get('volume', 0.8) * adjustments['fx_bus_volume_factor']
|
||
|
||
# Apply saturator_drive_factor to all bus saturators
|
||
if 'saturator_drive_factor' in adjustments:
|
||
for bus in buses:
|
||
fx_chain = bus.get('fx_chain', [])
|
||
saturator = find_device_in_chain(fx_chain, 'Saturator')
|
||
if saturator and 'Drive' in saturator.get('parameters', {}):
|
||
saturator['parameters']['Drive'] = (
|
||
saturator['parameters']['Drive'] * adjustments['saturator_drive_factor']
|
||
)
|
||
|
||
# Apply limiter_gain_factor to all bus limiters
|
||
if 'limiter_gain_factor' in adjustments:
|
||
for bus in buses:
|
||
fx_chain = bus.get('fx_chain', [])
|
||
limiter = find_device_in_chain(fx_chain, 'Limiter')
|
||
if limiter and 'Gain' in limiter.get('parameters', {}):
|
||
limiter['parameters']['Gain'] = (
|
||
limiter['parameters']['Gain'] * adjustments['limiter_gain_factor']
|
||
)
|
||
|
||
# Store final calibrated bus volumes
|
||
for bus in buses:
|
||
bus_key = bus.get('key', '')
|
||
if bus_key:
|
||
self._calibrated_bus_volumes[bus_key] = bus.get('volume', 0.0)
|
||
|
||
# RCA Fix: Automatic Makeup and Output gain compensation
|
||
for bus in buses:
|
||
for device in bus.get('fx_chain', []):
|
||
device_type = device.get('device')
|
||
params = device.get('parameters', {})
|
||
if device_type == 'Compressor' and 'Threshold' in params:
|
||
params['Makeup'] = round(abs(params['Threshold']) * 0.25, 1)
|
||
elif device_type == 'Saturator' and 'Drive' in params:
|
||
params['Output'] = round(-params['Drive'] * 1.5, 1)
|
||
|
||
return buses
|
||
|
||
def _build_return_blueprint(
|
||
self,
|
||
profile: Dict[str, Any],
|
||
genre: str,
|
||
style: str,
|
||
reference_resolution: Optional[Dict[str, Any]] = None,
|
||
) -> List[Dict[str, Any]]:
|
||
style_text = f"{genre} {style}".lower()
|
||
profile_name = str(profile.get('name', 'default')).lower()
|
||
reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()
|
||
returns = [
|
||
{
|
||
'name': 'MCP SPACE',
|
||
'send_key': 'space',
|
||
'color': 56,
|
||
'device_chain': [{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}}],
|
||
'volume': 0.76,
|
||
},
|
||
{
|
||
'name': 'MCP ECHO',
|
||
'send_key': 'echo',
|
||
'color': 44,
|
||
'device_chain': [{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}}],
|
||
'volume': 0.72,
|
||
},
|
||
{
|
||
'name': 'MCP HEAT',
|
||
'send_key': 'heat',
|
||
'color': 12,
|
||
'device_chain': [
|
||
{'device': 'Saturator', 'parameters': {'Drive': 4.5}},
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -16.0}},
|
||
],
|
||
'volume': 0.62,
|
||
},
|
||
{
|
||
'name': 'MCP GLUE',
|
||
'send_key': 'glue',
|
||
'color': 58,
|
||
'device_chain': [
|
||
{'device': 'Compressor', 'parameters': {'Threshold': -18.0}},
|
||
{'device': 'Limiter', 'parameters': {'Gain': 0.0}},
|
||
],
|
||
'volume': 0.68,
|
||
},
|
||
]
|
||
|
||
if profile_name == 'warehouse':
|
||
returns[0]['name'] = 'MCP BUNKER'
|
||
returns[0]['device_chain'] = [
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 7200.0, 'Dry/Wet': 0.22}},
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}},
|
||
]
|
||
returns[1]['name'] = 'MCP DUB'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 8200.0, 'Dry/Wet': 0.14}},
|
||
]
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = 5.5
|
||
returns[2]['volume'] = 0.66
|
||
elif profile_name == 'festival':
|
||
returns[0]['name'] = 'MCP WIDE'
|
||
returns[0]['device_chain'] = [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0}},
|
||
{'device': 'Utility', 'parameters': {'Stereo Width': 1.14}},
|
||
]
|
||
returns[1]['name'] = 'MCP TAIL'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.18}},
|
||
]
|
||
returns[0]['volume'] = 0.72
|
||
returns[1]['volume'] = 0.68
|
||
elif profile_name == 'swing':
|
||
returns[0]['name'] = 'MCP ROOM'
|
||
returns[1]['name'] = 'MCP SLAP'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 9800.0, 'Dry/Wet': 0.1}},
|
||
]
|
||
returns[2]['volume'] = 0.58
|
||
elif profile_name == 'jackin':
|
||
returns[0]['name'] = 'MCP CLUB'
|
||
returns[1]['name'] = 'MCP SWING'
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = 3.8
|
||
returns[3]['volume'] = 0.72
|
||
elif profile_name == 'tech-house-club':
|
||
# Short reverb, mono delay, wide FX for club tech-house
|
||
returns[0]['name'] = 'REVERB SHORT'
|
||
returns[0]['device_chain'] = [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 0.6}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 8400.0, 'Dry/Wet': 0.08}},
|
||
]
|
||
returns[0]['volume'] = 0.70
|
||
returns[1]['name'] = 'DELAY MONO'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Ping Pong': 0.0}},
|
||
{'device': 'Utility', 'parameters': {'Width': 0.0}},
|
||
]
|
||
returns[1]['volume'] = 0.68
|
||
returns[2]['name'] = 'DRIVE HOT'
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = 4.0
|
||
returns[2]['volume'] = 0.64
|
||
returns[3]['name'] = 'GLUE BUS'
|
||
returns[3]['device_chain'][0]['parameters']['Threshold'] = -16.5
|
||
returns[3]['volume'] = 0.70
|
||
elif profile_name == 'tech-house-deep':
|
||
# Deep minimal returns with subtle processing
|
||
returns[0]['name'] = 'REVERB DEEP'
|
||
returns[0]['device_chain'] = [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 1.2}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 6200.0, 'Dry/Wet': 0.12}},
|
||
]
|
||
returns[0]['volume'] = 0.72
|
||
returns[1]['name'] = 'DELAY DEEP'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Feedback': 0.45}},
|
||
]
|
||
returns[1]['volume'] = 0.64
|
||
returns[2]['name'] = 'SATURATE DEEP'
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = 2.5
|
||
returns[2]['volume'] = 0.56
|
||
returns[3]['name'] = 'GLUE MINIMAL'
|
||
returns[3]['device_chain'][0]['parameters']['Threshold'] = -20.0
|
||
returns[3]['volume'] = 0.62
|
||
elif profile_name == 'tech-house-funky':
|
||
# Groovy returns with modulation and swing
|
||
returns[0]['name'] = 'REVERB GROOVE'
|
||
returns[0]['device_chain'] = [
|
||
{'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 1.0, 'Decay Time': 0.8}},
|
||
{'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.08}},
|
||
]
|
||
returns[0]['volume'] = 0.74
|
||
returns[1]['name'] = 'DELAY GROOVE'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0, 'Ping Pong': 0.4, 'Feedback': 0.35}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 8000.0, 'Dry/Wet': 0.1}},
|
||
]
|
||
returns[1]['volume'] = 0.70
|
||
returns[2]['name'] = 'DRIVE FUNK'
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = 3.2
|
||
returns[2]['device_chain'].append({'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.06}})
|
||
returns[2]['volume'] = 0.60
|
||
returns[3]['name'] = 'GLUE SWING'
|
||
returns[3]['device_chain'][0]['parameters']['Threshold'] = -15.5
|
||
returns[3]['volume'] = 0.72
|
||
|
||
if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃmica', 'quimica']):
|
||
returns[1]['name'] = 'MCP VOX ECHO'
|
||
returns[1]['device_chain'] = [
|
||
{'device': 'Echo', 'parameters': {'Dry/Wet': 1.0}},
|
||
{'device': 'Auto Filter', 'parameters': {'Frequency': 10800.0, 'Dry/Wet': 0.12}},
|
||
]
|
||
returns[0]['volume'] = max(0.68, float(returns[0]['volume']) - 0.04)
|
||
if 'industrial' in style_text:
|
||
returns[2]['name'] = 'MCP DRIVE'
|
||
returns[2]['device_chain'][0]['parameters']['Drive'] = max(
|
||
4.8,
|
||
float(returns[2]['device_chain'][0]['parameters'].get('Drive', 4.5))
|
||
)
|
||
returns[3]['name'] = 'MCP BUS'
|
||
|
||
return returns
|
||
|
||
def _build_master_blueprint(
|
||
self,
|
||
profile: Dict[str, Any],
|
||
genre: str,
|
||
style: str,
|
||
reference_resolution: Optional[Dict[str, Any]] = None,
|
||
) -> Dict[str, Any]:
|
||
style_text = f"{genre} {style}".lower()
|
||
profile_name = str(profile.get('name', 'default')).lower()
|
||
reference_name = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()
|
||
|
||
# Start with default calibration values
|
||
calibration = dict(MASTER_CALIBRATION.get('default', {}))
|
||
|
||
# Find matching profile (case-insensitive, partial match)
|
||
matched_profile = 'default'
|
||
profile_name_lower = profile_name.lower()
|
||
for cal_key in MASTER_CALIBRATION.keys():
|
||
if cal_key.lower() in profile_name_lower or profile_name_lower in cal_key.lower():
|
||
# Merge profile-specific values over defaults
|
||
profile_cal = MASTER_CALIBRATION[cal_key]
|
||
calibration.update(profile_cal)
|
||
matched_profile = cal_key
|
||
break
|
||
|
||
# Track which profile was used
|
||
self._master_profile_used = matched_profile
|
||
|
||
# Build master with calibrated values
|
||
# Master chain: Utility (gain staging) -> Saturator (color) -> Compressor (glue) -> Limiter (ceiling)
|
||
# Target: -1dB peak before limiter, -0.3dBFS ceiling after limiter
|
||
master = {
|
||
'volume': calibration.get('volume', 0.85),
|
||
'device_chain': [
|
||
{
|
||
'device': 'Utility',
|
||
'parameters': {
|
||
'Gain': calibration.get('utility_gain', -0.5),
|
||
'Stereo Width': calibration.get('stereo_width', 1.04),
|
||
}
|
||
},
|
||
{
|
||
'device': 'Saturator',
|
||
'parameters': {'Drive': calibration.get('saturator_drive', 0.12)}
|
||
},
|
||
{
|
||
'device': 'Compressor',
|
||
'parameters': {
|
||
'Ratio': calibration.get('compressor_ratio', 0.50),
|
||
'Attack': calibration.get('compressor_attack', 0.30),
|
||
'Release': calibration.get('compressor_release', 0.20),
|
||
}
|
||
},
|
||
{
|
||
'device': 'Limiter',
|
||
'parameters': {
|
||
'Gain': calibration.get('limiter_gain', 0.8),
|
||
'Ceiling': calibration.get('limiter_ceiling', -0.3),
|
||
}
|
||
},
|
||
],
|
||
}
|
||
|
||
# Apply style-based limiter_gain_factor from STYLE_GAIN_ADJUSTMENTS
|
||
for style_key, style_adj in STYLE_GAIN_ADJUSTMENTS.items():
|
||
if style_key.lower() in style_text:
|
||
limiter_factor = style_adj.get('limiter_gain_factor')
|
||
if limiter_factor is not None:
|
||
master['device_chain'][3]['parameters']['Gain'] *= limiter_factor
|
||
break
|
||
|
||
if 'industrial' in style_text:
|
||
master['device_chain'][1]['parameters']['Drive'] = max(
|
||
0.8,
|
||
float(master['device_chain'][1]['parameters'].get('Drive', 0.3))
|
||
)
|
||
master['device_chain'][2]['parameters']['Ratio'] = max(
|
||
0.7,
|
||
float(master['device_chain'][2]['parameters'].get('Ratio', 0.62))
|
||
)
|
||
|
||
if 'latin' in style_text or any(term in reference_name for term in ['me gusta', 'quÃmica', 'quimica']):
|
||
master['device_chain'][0]['parameters']['Stereo Width'] = max(
|
||
1.14,
|
||
float(master['device_chain'][0]['parameters'].get('Stereo Width', 1.1))
|
||
)
|
||
master['device_chain'][3]['parameters']['Gain'] = max(
|
||
0.1,
|
||
float(master['device_chain'][3]['parameters'].get('Gain', 0.0))
|
||
)
|
||
|
||
return master
|
||
|
||
def _apply_role_gain_calibration(self, role: str, base_volume: float) -> Dict[str, float]:
|
||
"""
|
||
Apply ROLE_GAIN_CALIBRATION to a role's volume.
|
||
|
||
Args:
|
||
role: The role name (e.g., 'kick', 'bass', 'clap')
|
||
base_volume: The base volume from ROLE_MIX
|
||
|
||
Returns:
|
||
Dict with 'volume' and optionally 'saturator_drive' if calibrated
|
||
"""
|
||
if role not in ROLE_GAIN_CALIBRATION:
|
||
return {'volume': base_volume}
|
||
|
||
calibration = ROLE_GAIN_CALIBRATION[role]
|
||
calibrated_volume = float(calibration.get('volume', base_volume))
|
||
|
||
# Apply peak_reduction if present
|
||
peak_reduction = calibration.get('peak_reduction', 0.0)
|
||
if peak_reduction > 0:
|
||
calibrated_volume *= (1.0 - float(peak_reduction))
|
||
self._peak_reductions_count += 1
|
||
|
||
result = {'volume': round(max(0.0, min(1.0, calibrated_volume)), 3)}
|
||
|
||
# Include saturator_drive if present in calibration
|
||
if 'saturator_drive' in calibration:
|
||
result['saturator_drive'] = float(calibration['saturator_drive'])
|
||
|
||
self._gain_calibration_overrides_count += 1
|
||
|
||
return result
|
||
|
||
def _shape_mix_profile(self, role: str, mix_profile: Dict[str, Any], profile: Dict[str, Any], style: str) -> Dict[str, Any]:
|
||
shaped = {
|
||
'volume': float(mix_profile.get('volume', 0.72)),
|
||
'pan': float(mix_profile.get('pan', 0.0)),
|
||
'sends': dict(mix_profile.get('sends', {})),
|
||
}
|
||
|
||
# Apply ROLE_GAIN_CALIBRATION if available - overrides base volume
|
||
calibration = self._apply_role_gain_calibration(role, shaped['volume'])
|
||
if calibration.get('volume') is not None:
|
||
shaped['volume'] = calibration['volume']
|
||
if calibration.get('saturator_drive') is not None:
|
||
shaped['saturator_drive'] = calibration['saturator_drive']
|
||
|
||
profile_name = str(profile.get('name', 'default')).lower()
|
||
pan_width = float(profile.get('pan_width', 0.16) or 0.16)
|
||
style_text = str(style or '').lower()
|
||
|
||
if role in ['hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'pluck', 'arp', 'counter', 'vocal']:
|
||
shaped['pan'] = max(-1.0, min(1.0, shaped['pan'] * (1.0 + pan_width)))
|
||
|
||
if profile_name == 'warehouse':
|
||
if role in ['kick', 'bass', 'sub_bass']:
|
||
shaped['volume'] *= 1.03
|
||
if role in ['pad', 'drone', 'atmos']:
|
||
shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 0.88
|
||
if role in ['reverse_fx', 'riser', 'impact']:
|
||
shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.08)
|
||
elif profile_name == 'festival':
|
||
if role in ['lead', 'chords', 'pad', 'arp', 'vocal']:
|
||
shaped['volume'] *= 1.04
|
||
shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.15
|
||
if role in ['kick', 'clap']:
|
||
shaped['sends']['glue'] = max(shaped['sends'].get('glue', 0.0), 0.12)
|
||
elif profile_name == 'swing':
|
||
if role in ['perc', 'top_loop', 'ride', 'vocal', 'pluck']:
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.14
|
||
if role in ['kick', 'sub_bass']:
|
||
shaped['volume'] *= 0.98
|
||
elif profile_name == 'jackin':
|
||
if role in ['clap', 'perc', 'vocal', 'counter']:
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.08
|
||
if role in ['top_loop', 'ride']:
|
||
shaped['volume'] *= 1.03
|
||
elif profile_name == 'tech-house-club':
|
||
# Club-oriented: punchy drums, present vocals, tight bass
|
||
if role in ['kick', 'clap']:
|
||
shaped['volume'] *= 1.02
|
||
shaped['sends']['glue'] = max(shaped['sends'].get('glue', 0.0), 0.10)
|
||
if role in ['bass', 'sub_bass']:
|
||
shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.06)
|
||
if role in ['vocal', 'counter']:
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.10
|
||
if role in ['hat_open', 'top_loop', 'ride']:
|
||
shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 0.92
|
||
elif profile_name == 'tech-house-deep':
|
||
# Deep minimal: subtle processing, wide stereo
|
||
if role in ['kick', 'sub_bass']:
|
||
shaped['volume'] *= 0.98
|
||
if role in ['pad', 'drone', 'atmos', 'chords']:
|
||
shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.12
|
||
if role in ['perc', 'top_loop']:
|
||
shaped['volume'] *= 0.95
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 0.88
|
||
elif profile_name == 'tech-house-funky':
|
||
# Funky groove: wider pan, more echo, bouncy feel
|
||
if role in ['perc', 'top_loop', 'ride']:
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.18
|
||
if role in ['bass', 'sub_bass']:
|
||
shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.05)
|
||
if role in ['vocal', 'pluck', 'arp']:
|
||
shaped['sends']['space'] = shaped['sends'].get('space', 0.0) * 1.08
|
||
if role in ['clap', 'hat_closed']:
|
||
shaped['volume'] *= 1.02
|
||
|
||
if 'latin' in style_text and role in ['perc', 'top_loop', 'ride', 'vocal']:
|
||
shaped['sends']['echo'] = shaped['sends'].get('echo', 0.0) * 1.12
|
||
shaped['pan'] = max(-1.0, min(1.0, shaped['pan'] * 1.08))
|
||
if 'industrial' in style_text and role in ['kick', 'bass', 'stab', 'impact', 'riser']:
|
||
shaped['sends']['heat'] = max(shaped['sends'].get('heat', 0.0), 0.09)
|
||
|
||
shaped['volume'] = round(max(0.0, min(1.0, shaped['volume'])), 3)
|
||
shaped['pan'] = round(max(-1.0, min(1.0, shaped['pan'])), 3)
|
||
shaped['sends'] = {
|
||
send_key: round(max(0.0, min(1.0, float(send_value))), 3)
|
||
for send_key, send_value in shaped['sends'].items()
|
||
}
|
||
return shaped
|
||
|
||
def _shape_role_fx_chain(self, role: str, profile: Dict[str, Any], style: str) -> List[Dict[str, Any]]:
|
||
chain = [dict(item) for item in ROLE_FX_CHAINS.get(role, [])]
|
||
profile_name = str(profile.get('name', 'default')).lower()
|
||
style_text = str(style or '').lower()
|
||
|
||
if profile_name == 'warehouse':
|
||
if role in ['kick', 'bass', 'stab']:
|
||
chain.append({'device': 'Compressor', 'parameters': {'Threshold': -18.0}})
|
||
if role in ['atmos', 'drone', 'pad']:
|
||
chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 7600.0, 'Dry/Wet': 0.14}})
|
||
elif profile_name == 'festival':
|
||
if role in ['lead', 'arp', 'vocal']:
|
||
chain.append({'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.1}})
|
||
if role in ['chords', 'pad']:
|
||
chain.append({'device': 'Utility', 'parameters': {'Width': 140.0}})
|
||
elif profile_name == 'swing':
|
||
if role in ['perc', 'top_loop', 'ride', 'vocal']:
|
||
chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}})
|
||
elif profile_name == 'jackin':
|
||
if role in ['clap', 'perc', 'vocal', 'counter']:
|
||
chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.5}})
|
||
elif profile_name == 'tech-house-club':
|
||
# Club: punchy drums, saturated bass, crisp tops
|
||
if role in ['kick', 'clap']:
|
||
chain.append({'device': 'Compressor', 'parameters': {'Threshold': -16.0, 'Attack': 0.02}})
|
||
if role in ['bass', 'sub_bass']:
|
||
chain.append({'device': 'Saturator', 'parameters': {'Drive': 2.0}})
|
||
if role in ['hat_closed', 'hat_open', 'top_loop']:
|
||
chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 12000.0, 'Dry/Wet': 0.12}})
|
||
if role in ['vocal', 'counter']:
|
||
chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.08}})
|
||
elif profile_name == 'tech-house-deep':
|
||
# Deep: subtle saturation, atmospheric processing
|
||
if role in ['kick', 'bass']:
|
||
chain.append({'device': 'Compressor', 'parameters': {'Threshold': -20.0}})
|
||
if role in ['pad', 'drone', 'atmos']:
|
||
chain.append({'device': 'Hybrid Reverb', 'parameters': {'Dry/Wet': 0.12}})
|
||
if role in ['chords', 'pluck']:
|
||
chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 9200.0, 'Dry/Wet': 0.08}})
|
||
elif profile_name == 'tech-house-funky':
|
||
# Funky: groove-enhancing FX, modulation
|
||
if role in ['perc', 'top_loop', 'ride']:
|
||
chain.append({'device': 'Echo', 'parameters': {'Dry/Wet': 0.10, 'Ping Pong': 0.3}})
|
||
if role in ['bass', 'sub_bass']:
|
||
chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.8}})
|
||
if role in ['vocal', 'pluck', 'arp']:
|
||
chain.append({'device': 'Chorus-Ensemble', 'parameters': {'Dry/Wet': 0.06}})
|
||
if role in ['clap', 'hat_closed']:
|
||
chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.2}})
|
||
|
||
if 'industrial' in style_text and role in ['kick', 'bass', 'impact', 'riser']:
|
||
chain.append({'device': 'Saturator', 'parameters': {'Drive': 1.8}})
|
||
if 'latin' in style_text and role in ['perc', 'top_loop', 'ride', 'vocal']:
|
||
chain.append({'device': 'Auto Filter', 'parameters': {'Frequency': 11200.0, 'Dry/Wet': 0.1}})
|
||
|
||
return chain
|
||
|
||
def _get_section_drum_variant(self, role: str, section: Dict[str, Any]) -> str:
|
||
"""Get appropriate drum variant for section and role with cross-generation diversity."""
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
role_lower = role.lower()
|
||
|
||
if role_lower not in DRUM_SECTION_VARIANTS.get(kind, {}):
|
||
return 'straight'
|
||
|
||
variants = list(DRUM_SECTION_VARIANTS[kind][role_lower])
|
||
valid_variants = [v for v in variants if v in DRUM_PATTERN_BANKS.get(role_lower, {})]
|
||
if not valid_variants and role_lower in DRUM_PATTERN_BANKS:
|
||
valid_variants = list(DRUM_PATTERN_BANKS[role_lower].keys())
|
||
|
||
if not valid_variants:
|
||
return 'straight'
|
||
|
||
rng = self._section_rng(section, role, salt=1)
|
||
|
||
if len(valid_variants) > 1:
|
||
scored_variants = []
|
||
for v in valid_variants:
|
||
penalty = _get_pattern_variant_penalty('drum', f'{role_lower}_{v}')
|
||
score = rng.random() - penalty
|
||
scored_variants.append((score, v))
|
||
scored_variants.sort(reverse=True)
|
||
chosen = scored_variants[0][1]
|
||
else:
|
||
chosen = valid_variants[0]
|
||
|
||
_record_pattern_variant_usage('drum', f'{role_lower}_{chosen}')
|
||
return chosen
|
||
|
||
def _generate_drum_pattern_from_bank(self, role: str, variant: str,
|
||
section_length: float,
|
||
velocity_base: int = 100) -> List[Dict[str, Any]]:
|
||
"""Generate drum pattern from pattern bank."""
|
||
role_lower = role.lower()
|
||
|
||
if role_lower not in DRUM_PATTERN_BANKS:
|
||
return []
|
||
|
||
bank = DRUM_PATTERN_BANKS[role_lower]
|
||
if variant not in bank:
|
||
variant = list(bank.keys())[0] # Fallback to first
|
||
|
||
positions = bank[variant]
|
||
notes = []
|
||
|
||
# Determine pitch based on role
|
||
pitch_map = {
|
||
'kick': 36, 'clap': 39, 'hat_closed': 42,
|
||
'hat_open': 46, 'perc': 50, 'ride': 51
|
||
}
|
||
pitch = pitch_map.get(role_lower, 36)
|
||
|
||
for pos in positions:
|
||
# Repeat pattern for each bar
|
||
for bar in range(int(section_length // 4)):
|
||
start = pos + (bar * 4.0)
|
||
if start < section_length:
|
||
# Add slight velocity variation
|
||
velocity = max(60, min(127, velocity_base + random.randint(-10, 10)))
|
||
duration = 0.1 if role_lower in ['hat_closed', 'hat_open', 'ride'] else 0.15
|
||
notes.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
logger.debug(f"Generated drum pattern from bank: role={role}, variant={variant}, notes={len(notes)}")
|
||
return notes
|
||
|
||
def _get_section_bass_variant(self, section: Dict[str, Any]) -> str:
|
||
"""Get appropriate bass variant for section with cross-generation diversity."""
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
|
||
if kind not in BASS_SECTION_VARIANTS:
|
||
return 'anchor'
|
||
|
||
variants = list(BASS_SECTION_VARIANTS[kind])
|
||
valid_variants = [v for v in variants if v in BASS_PATTERN_BANKS]
|
||
if not valid_variants:
|
||
valid_variants = list(BASS_PATTERN_BANKS.keys())
|
||
|
||
rng = self._section_rng(section, 'bass', salt=2)
|
||
|
||
if len(valid_variants) > 1:
|
||
scored_variants = []
|
||
for v in valid_variants:
|
||
penalty = _get_pattern_variant_penalty('bass', v)
|
||
score = rng.random() - penalty
|
||
scored_variants.append((score, v))
|
||
scored_variants.sort(reverse=True)
|
||
chosen = scored_variants[0][1]
|
||
else:
|
||
chosen = valid_variants[0] if valid_variants else 'anchor'
|
||
|
||
_record_pattern_variant_usage('bass', chosen)
|
||
return chosen
|
||
|
||
def _compute_section_signature(self, section: Dict[str, Any]) -> str:
|
||
"""Compute a signature for section to detect repetition."""
|
||
section = self._ensure_section_pattern_variants(section)
|
||
signature_parts = []
|
||
drum_role_variants = dict(section.get('drum_role_variants') or {})
|
||
|
||
signature_parts.append(f"kick:{drum_role_variants.get('kick', section.get('drum_variant', 'default'))}")
|
||
signature_parts.append(f"clap:{drum_role_variants.get('clap', section.get('drum_variant', 'default'))}")
|
||
signature_parts.append(f"hat:{drum_role_variants.get('hat_closed', section.get('drum_variant', 'default'))}")
|
||
signature_parts.append(f"bass:{section.get('bass_bank_variant', section.get('bass_variant', 'default'))}")
|
||
signature_parts.append(f"lead:{section.get('melodic_bank_variant', section.get('melodic_variant', 'default'))}")
|
||
signature_parts.append(f"fill:{section.get('transition_fill', 'none')}")
|
||
|
||
# Add density and swing
|
||
density = section.get('density', 1.0)
|
||
swing = section.get('swing', 0.0)
|
||
signature_parts.append(f"d:{density:.1f}")
|
||
signature_parts.append(f"s:{swing:.2f}")
|
||
|
||
return "|".join(signature_parts)
|
||
|
||
def _check_section_repetition(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""Check and warn about excessive section repetition."""
|
||
signatures = []
|
||
consecutive_same = 0
|
||
max_consecutive = 2
|
||
|
||
for i, section in enumerate(sections):
|
||
self._ensure_section_pattern_variants(section)
|
||
sig = self._compute_section_signature(section)
|
||
|
||
if signatures and signatures[-1] == sig:
|
||
consecutive_same += 1
|
||
if consecutive_same >= max_consecutive:
|
||
logger.warning("REPETITION: %d consecutive sections with same signature: %s",
|
||
consecutive_same + 1, sig)
|
||
self._force_section_pattern_variation(section)
|
||
sig = self._compute_section_signature(section)
|
||
else:
|
||
consecutive_same = 0
|
||
|
||
signatures.append(sig)
|
||
|
||
return sections
|
||
|
||
def _record_section_variant(self, section: Dict[str, Any], role: str, variant: str):
|
||
"""Record variant used for a role in a section."""
|
||
key = f'{role}_variant'
|
||
section[key] = variant
|
||
|
||
def _choose_alternate_variant(self, options: List[str], current: Optional[str], rng: random.Random) -> Optional[str]:
|
||
ordered: List[str] = []
|
||
for option in options:
|
||
if option not in ordered:
|
||
ordered.append(option)
|
||
if not ordered:
|
||
return current
|
||
alternatives = [option for option in ordered if option != current]
|
||
if not alternatives:
|
||
return current or ordered[0]
|
||
return rng.choice(alternatives)
|
||
|
||
def _ensure_section_pattern_variants(self, section: Dict[str, Any]) -> Dict[str, Any]:
|
||
_kind = str(section.get('kind', 'drop')).lower() # noqa: F841 - used by helper methods via section dict
|
||
drum_role_variants = dict(section.get('drum_role_variants') or {})
|
||
for role in ['kick', 'clap', 'hat_closed', 'hat_open', 'perc', 'ride']:
|
||
if role in drum_role_variants:
|
||
continue
|
||
variant = self._get_section_drum_variant(role, section)
|
||
if variant in DRUM_PATTERN_BANKS.get(role, {}):
|
||
drum_role_variants[role] = variant
|
||
self._record_section_variant(section, role, variant)
|
||
section['drum_role_variants'] = drum_role_variants
|
||
|
||
bass_bank_variant = str(section.get('bass_bank_variant', '') or '')
|
||
if bass_bank_variant not in BASS_PATTERN_BANKS:
|
||
bass_bank_variant = self._get_section_bass_variant(section)
|
||
section['bass_bank_variant'] = bass_bank_variant
|
||
self._record_section_variant(section, 'bass_bank', str(section.get('bass_bank_variant', 'anchor')))
|
||
|
||
melodic_bank_variant = str(section.get('melodic_bank_variant', '') or '')
|
||
if melodic_bank_variant not in MELODIC_PATTERN_BANKS:
|
||
melodic_bank_variant = self._get_section_melodic_variant(section)
|
||
section['melodic_bank_variant'] = melodic_bank_variant
|
||
self._record_section_variant(section, 'melodic_bank', str(section.get('melodic_bank_variant', 'motif')))
|
||
section.setdefault('pattern_variant_ready', True)
|
||
return section
|
||
|
||
def _force_section_pattern_variation(self, section: Dict[str, Any]) -> Dict[str, Any]:
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
self._ensure_section_pattern_variants(section)
|
||
drum_role_variants = dict(section.get('drum_role_variants') or {})
|
||
|
||
for role in ['kick', 'clap', 'hat_closed']:
|
||
options = DRUM_SECTION_VARIANTS.get(kind, {}).get(role, [])
|
||
current = drum_role_variants.get(role)
|
||
next_variant = self._choose_alternate_variant(options, current, self._section_rng(section, role, salt=101))
|
||
if next_variant:
|
||
drum_role_variants[role] = next_variant
|
||
self._record_section_variant(section, role, next_variant)
|
||
section['drum_role_variants'] = drum_role_variants
|
||
|
||
bass_options = BASS_SECTION_VARIANTS.get(kind, [])
|
||
bass_variant = self._choose_alternate_variant(
|
||
bass_options,
|
||
str(section.get('bass_bank_variant', '') or ''),
|
||
self._section_rng(section, 'bass', salt=102),
|
||
)
|
||
if bass_variant:
|
||
section['bass_bank_variant'] = bass_variant
|
||
self._record_section_variant(section, 'bass_bank', bass_variant)
|
||
|
||
melodic_options = MELODIC_SECTION_VARIANTS.get(kind, [])
|
||
melodic_variant = self._choose_alternate_variant(
|
||
melodic_options,
|
||
str(section.get('melodic_bank_variant', '') or ''),
|
||
self._section_rng(section, 'melodic', salt=103),
|
||
)
|
||
if melodic_variant:
|
||
section['melodic_bank_variant'] = melodic_variant
|
||
self._record_section_variant(section, 'melodic_bank', melodic_variant)
|
||
|
||
return section
|
||
|
||
def _generate_bass_pattern_from_bank(self, variant: str, key: str,
|
||
section_length: float,
|
||
velocity_base: int = 95) -> List[Dict[str, Any]]:
|
||
"""Generate bass pattern from pattern bank."""
|
||
if variant not in BASS_PATTERN_BANKS:
|
||
variant = 'anchor'
|
||
|
||
bank = BASS_PATTERN_BANKS[variant]
|
||
positions = bank['positions']
|
||
durations = bank['durations']
|
||
style = bank.get('style', 'root')
|
||
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, 2)
|
||
|
||
notes = []
|
||
for bar in range(int(section_length // 4)):
|
||
for i, pos in enumerate(positions):
|
||
start = pos + (bar * 4.0)
|
||
if start < section_length:
|
||
duration = durations[i] if i < len(durations) else 0.4
|
||
velocity = max(70, min(120, velocity_base + random.randint(-8, 8)))
|
||
|
||
# Adjust pitch based on style
|
||
pitch = root_midi
|
||
if style == 'ascending' and bar > 0:
|
||
pitch += min(bar, 5) # Rise over bars
|
||
elif style == 'syncopated' and i % 2 == 1:
|
||
pitch += 5 # Fifth on offbeats
|
||
|
||
notes.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
logger.debug(f"Generated bass pattern from bank: variant={variant}, notes={len(notes)}")
|
||
return notes
|
||
|
||
def _vary_drum_notes(self, notes: List[Dict[str, Any]], role: str, section: Dict[str, Any],
|
||
section_length: float) -> List[Dict[str, Any]]:
|
||
section = self._ensure_section_pattern_variants(section)
|
||
role_variant = str((section.get('drum_role_variants') or {}).get(role, '') or '').lower()
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
density = float(section.get('density', 1.0))
|
||
_ = int(section.get('energy', 1))
|
||
variant = str(section.get('drum_variant', 'straight')).lower()
|
||
swing = float(section.get('swing', 0.0))
|
||
tightness = float(self._current_generation_profile.get('drum_tightness', 1.0))
|
||
rng = self._section_rng(section, role, salt=5)
|
||
|
||
if role_variant in DRUM_PATTERN_BANKS.get(role, {}):
|
||
logger.debug(f"Using section pattern bank for {role} with variant {role_variant} in section {kind}")
|
||
bank_notes = self._generate_drum_pattern_from_bank(role, role_variant, section_length)
|
||
if bank_notes:
|
||
use_bank_prob = 0.85 if kind in ['intro', 'break', 'outro'] else 0.95
|
||
if rng.random() < use_bank_prob or not notes:
|
||
return bank_notes
|
||
|
||
if not notes:
|
||
if role in DRUM_PATTERN_BANKS:
|
||
all_variants = list(DRUM_PATTERN_BANKS[role].keys())
|
||
if all_variants:
|
||
fallback_variant = rng.choice(all_variants)
|
||
return self._generate_drum_pattern_from_bank(role, fallback_variant, section_length)
|
||
return []
|
||
|
||
varied = list(notes)
|
||
|
||
if variant == 'skip' and role in ['hat_closed', 'hat_open', 'top_loop', 'perc', 'ride']:
|
||
varied = self._apply_density_mask(varied, section, role, keep_probability=min(0.94, max(0.54, density - 0.08)))
|
||
elif variant == 'pressure' and role in ['kick', 'hat_closed', 'perc']:
|
||
pressure_notes = []
|
||
for bar_start in range(0, int(section_length), 4):
|
||
if role == 'kick' and rng.random() > 0.35:
|
||
pressure_notes.append(self._make_note(36, min(section_length - 0.05, bar_start + 3.5), 0.12, 92))
|
||
elif role == 'hat_closed' and rng.random() > 0.45:
|
||
pressure_notes.append(self._make_note(42, min(section_length - 0.05, bar_start + 3.75), 0.06, 58))
|
||
elif role == 'perc' and rng.random() > 0.5:
|
||
pressure_notes.append(self._make_note(50, min(section_length - 0.05, bar_start + 3.25), 0.12, 74))
|
||
varied = self._merge_section_notes(varied, pressure_notes, section_length)
|
||
elif variant == 'shuffle' and role not in ['kick', 'clap', 'sc_trigger', 'crash']:
|
||
varied = self._apply_swing(varied, swing or (0.035 / max(0.8, tightness)), section_length)
|
||
|
||
if swing > 0.0 and role in ['top_loop', 'perc', 'ride']:
|
||
varied = self._apply_swing(varied, swing * 0.55, section_length)
|
||
|
||
return varied
|
||
|
||
def _vary_bass_notes(self, notes: List[Dict[str, Any]], role: str, key: str,
|
||
section: Dict[str, Any], section_length: float) -> List[Dict[str, Any]]:
|
||
section = self._ensure_section_pattern_variants(section)
|
||
bank_variant = str(section.get('bass_bank_variant', '') or '').lower()
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
variant = str(section.get('bass_variant', 'anchor')).lower()
|
||
|
||
if bank_variant in BASS_PATTERN_BANKS:
|
||
logger.debug(f"Using section bass pattern bank for variant {bank_variant} in section {kind}")
|
||
return self._generate_bass_pattern_from_bank(bank_variant, key, section_length)
|
||
|
||
if not notes:
|
||
if bank_variant in BASS_PATTERN_BANKS:
|
||
return self._generate_bass_pattern_from_bank(bank_variant, key, section_length)
|
||
all_variants = list(BASS_PATTERN_BANKS.keys())
|
||
if all_variants:
|
||
rng = self._section_rng(section, role, salt=7)
|
||
fallback = rng.choice(all_variants)
|
||
return self._generate_bass_pattern_from_bank(fallback, key, section_length)
|
||
return []
|
||
|
||
profile_motion = str(self._current_generation_profile.get('bass_motion', 'locked')).lower()
|
||
rng = self._section_rng(section, role, salt=7)
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
scale_name = 'minor' if 'm' in key.lower() else 'major'
|
||
root_midi = self.note_name_to_midi(root_note, 2)
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
|
||
varied = []
|
||
for index, note in enumerate(notes):
|
||
pitch = int(note['pitch'])
|
||
start = float(note['start'])
|
||
duration = float(note['duration'])
|
||
velocity = int(note['velocity'])
|
||
|
||
if variant == 'anchor' and (start % 4.0) < 0.001:
|
||
pitch = root_midi
|
||
duration = max(duration, 0.5)
|
||
elif variant == 'bounce' and (start % 1.0) >= 0.5:
|
||
velocity = min(124, velocity + 8)
|
||
duration = max(0.18, duration * 0.82)
|
||
elif variant == 'syncopated' and (start % 1.0) < 0.001 and rng.random() > 0.4:
|
||
start = min(section_length - 0.05, start + 0.25)
|
||
duration = max(0.16, duration * 0.68)
|
||
elif variant == 'pedal' and index % 3 == 0:
|
||
pitch = root_midi
|
||
|
||
if profile_motion == 'lifted' and index % 8 == 6:
|
||
pitch += 12
|
||
elif profile_motion == 'syncopated' and rng.random() > 0.72:
|
||
pitch = scale_notes[(index + 4) % len(scale_notes)]
|
||
elif profile_motion == 'bouncy' and (start % 4.0) >= 2.0:
|
||
velocity = min(124, velocity + 5)
|
||
|
||
varied.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
return self._shape_notes_for_section(varied, kind, role, section_length)
|
||
|
||
def _get_section_melodic_variant(self, section: Dict[str, Any]) -> str:
|
||
"""Get appropriate melodic variant for section with cross-generation diversity."""
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
|
||
if kind not in MELODIC_SECTION_VARIANTS:
|
||
return 'motif'
|
||
|
||
variants = list(MELODIC_SECTION_VARIANTS[kind])
|
||
valid_variants = [v for v in variants if v in MELODIC_PATTERN_BANKS]
|
||
if not valid_variants:
|
||
valid_variants = list(MELODIC_PATTERN_BANKS.keys())
|
||
|
||
rng = self._section_rng(section, 'melodic', salt=3)
|
||
|
||
if len(valid_variants) > 1:
|
||
scored_variants = []
|
||
for v in valid_variants:
|
||
penalty = _get_pattern_variant_penalty('melodic', v)
|
||
score = rng.random() - penalty
|
||
scored_variants.append((score, v))
|
||
scored_variants.sort(reverse=True)
|
||
chosen = scored_variants[0][1]
|
||
else:
|
||
chosen = valid_variants[0] if valid_variants else 'motif'
|
||
|
||
_record_pattern_variant_usage('melodic', chosen)
|
||
return chosen
|
||
|
||
def _generate_melodic_pattern_from_bank(self, variant: str, key: str,
|
||
scale_name: str,
|
||
section_length: float,
|
||
velocity_base: int = 90) -> List[Dict[str, Any]]:
|
||
"""Generate melodic pattern from pattern bank."""
|
||
if variant not in MELODIC_PATTERN_BANKS:
|
||
variant = 'motif'
|
||
|
||
bank = MELODIC_PATTERN_BANKS[variant]
|
||
intervals = bank['intervals']
|
||
rhythm = bank['rhythm']
|
||
durations = bank['durations']
|
||
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, 5)
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
|
||
notes = []
|
||
for bar in range(int(section_length // 4)):
|
||
for i, pos in enumerate(rhythm):
|
||
start = pos + (bar * 4.0)
|
||
if start < section_length:
|
||
interval = intervals[i] if i < len(intervals) else intervals[-1]
|
||
pitch = scale_notes[interval % len(scale_notes)]
|
||
duration = durations[i] if i < len(durations) else 0.3
|
||
velocity = max(60, min(110, velocity_base + random.randint(-10, 10)))
|
||
|
||
notes.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
logger.debug(f"Generated melodic pattern from bank: variant={variant}, notes={len(notes)}")
|
||
return notes
|
||
|
||
def _vary_melodic_notes(self, notes: List[Dict[str, Any]], role: str, key: str, scale_name: str,
|
||
section: Dict[str, Any], section_length: float) -> List[Dict[str, Any]]:
|
||
section = self._ensure_section_pattern_variants(section)
|
||
bank_variant = str(section.get('melodic_bank_variant', '') or '').lower()
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
|
||
if bank_variant in MELODIC_PATTERN_BANKS:
|
||
logger.debug(f"Using section melodic pattern bank for variant {bank_variant} in section {kind}")
|
||
return self._generate_melodic_pattern_from_bank(bank_variant, key, scale_name, section_length)
|
||
|
||
if not notes:
|
||
if bank_variant in MELODIC_PATTERN_BANKS:
|
||
return self._generate_melodic_pattern_from_bank(bank_variant, key, scale_name, section_length)
|
||
all_variants = list(MELODIC_PATTERN_BANKS.keys())
|
||
if all_variants:
|
||
rng = self._section_rng(section, role, salt=11)
|
||
fallback = rng.choice(all_variants)
|
||
return self._generate_melodic_pattern_from_bank(fallback, key, scale_name, section_length)
|
||
return []
|
||
|
||
variant = str(section.get('melodic_variant', 'motif')).lower()
|
||
profile_motion = str(self._current_generation_profile.get('melodic_motion', 'restrained')).lower()
|
||
rng = self._section_rng(section, role, salt=11)
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, 5)
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
|
||
transformed = []
|
||
for index, note in enumerate(notes):
|
||
start = float(note['start'])
|
||
pitch = int(note['pitch'])
|
||
duration = float(note['duration'])
|
||
velocity = int(note['velocity'])
|
||
keep = True
|
||
|
||
if variant == 'response' and int(start / 2.0) % 2 == 0 and role in ['lead', 'pluck', 'counter']:
|
||
keep = False
|
||
elif variant == 'lift' and index % 4 == 3:
|
||
pitch += 12
|
||
velocity = min(124, velocity + 10)
|
||
elif variant == 'descend' and index % 5 == 4:
|
||
pitch -= 12
|
||
duration = max(0.16, duration * 0.9)
|
||
elif variant == 'drone':
|
||
keep = (start % 4.0) < 0.001 or duration >= 0.5
|
||
if keep:
|
||
pitch = scale_notes[index % min(3, len(scale_notes))]
|
||
duration = max(duration, 1.2)
|
||
|
||
if keep and profile_motion in ['anthemic', 'hooky'] and role in ['lead', 'arp', 'pluck']:
|
||
if rng.random() > 0.78:
|
||
pitch += 12
|
||
elif profile_motion == 'hooky' and rng.random() > 0.84:
|
||
start = min(section_length - 0.05, start + 0.25)
|
||
|
||
if keep and profile_motion == 'call_response' and role in ['counter', 'pluck'] and (start % 4.0) < 2.0:
|
||
velocity = max(52, velocity - 8)
|
||
|
||
if keep:
|
||
transformed.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
if role in ['arp', 'pluck'] and float(section.get('swing', 0.0)) > 0.0:
|
||
transformed = self._apply_swing(transformed, float(section.get('swing', 0.0)) * 0.45, section_length)
|
||
|
||
return self._shape_notes_for_section(transformed, kind, role, section_length)
|
||
|
||
def _transpose_notes(self, notes: List[Dict[str, Any]], semitones: int) -> List[Dict[str, Any]]:
|
||
return [
|
||
self._make_note(note['pitch'] + semitones, note['start'], note['duration'], note['velocity'])
|
||
for note in notes
|
||
]
|
||
|
||
def _scale_note_lengths(self, notes: List[Dict[str, Any]], factor: float, minimum: float = 0.1) -> List[Dict[str, Any]]:
|
||
scaled = []
|
||
for note in notes:
|
||
scaled.append(
|
||
self._make_note(
|
||
note['pitch'],
|
||
note['start'],
|
||
max(minimum, float(note['duration']) * factor),
|
||
note['velocity'],
|
||
)
|
||
)
|
||
return scaled
|
||
|
||
def _shape_notes_for_section(self, notes: List[Dict[str, Any]], section_kind: str, role: str,
|
||
section_length: float) -> List[Dict[str, Any]]:
|
||
if not notes:
|
||
return []
|
||
|
||
shaped = []
|
||
for note in notes:
|
||
start = float(note['start'])
|
||
keep = True
|
||
|
||
if section_kind in ['intro', 'outro'] and role in ['bass', 'sub_bass', 'lead', 'pluck', 'arp', 'counter']:
|
||
keep = int(start * 2) % 4 == 0
|
||
elif section_kind == 'break' and role in ['bass', 'sub_bass', 'lead', 'pluck', 'arp', 'counter', 'clap', 'hat_open', 'ride']:
|
||
keep = int(start) % 4 == 0
|
||
|
||
if keep and start < section_length:
|
||
duration = min(float(note['duration']), section_length - start)
|
||
shaped.append(self._make_note(note['pitch'], start, duration, note['velocity']))
|
||
return shaped
|
||
|
||
def _merge_section_notes(self, base_notes: List[Dict[str, Any]], extra_notes: List[Dict[str, Any]],
|
||
section_length: float) -> List[Dict[str, Any]]:
|
||
merged = []
|
||
for note in list(base_notes) + list(extra_notes):
|
||
start = float(note['start'])
|
||
if start >= section_length:
|
||
continue
|
||
duration = min(float(note['duration']), max(0.05, section_length - start))
|
||
merged.append(self._make_note(note['pitch'], start, duration, note['velocity']))
|
||
merged.sort(key=lambda item: (item['start'], item['pitch']))
|
||
return merged
|
||
|
||
def _build_drum_fill(self, role: str, section_length: float, intensity: int) -> List[Dict[str, Any]]:
|
||
fill_start = max(0.0, section_length - 1.0)
|
||
if role == 'kick' and intensity >= 3:
|
||
return [self._make_note(36, fill_start + step, 0.14, 112 + (idx % 2) * 8) for idx, step in enumerate([0.0, 0.25, 0.5, 0.75])]
|
||
if role == 'clap' and intensity >= 3:
|
||
return [self._make_note(39, fill_start + step, 0.18, 92 + idx * 6) for idx, step in enumerate([0.25, 0.5, 0.75])]
|
||
if role == 'hat_closed':
|
||
return [self._make_note(42, fill_start + (idx * 0.125), 0.06, 64 + (idx % 4) * 6) for idx in range(8)]
|
||
if role == 'perc' and intensity >= 2:
|
||
return [
|
||
self._make_note(37, fill_start + 0.125, 0.08, 72),
|
||
self._make_note(47, fill_start + 0.375, 0.08, 76),
|
||
self._make_note(50, fill_start + 0.625, 0.1, 82),
|
||
]
|
||
return []
|
||
|
||
def _build_turnaround_notes(self, key: str, scale_name: str, section_length: float,
|
||
octave: int, velocity: int = 92) -> List[Dict[str, Any]]:
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, octave)
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
fill_start = max(0.0, section_length - 2.0)
|
||
degrees = [0, 2, 4, 6]
|
||
notes = []
|
||
for index, degree in enumerate(degrees):
|
||
pitch = scale_notes[degree % len(scale_notes)]
|
||
notes.append(self._make_note(pitch, fill_start + (index * 0.5), 0.38, velocity + index * 4))
|
||
return notes
|
||
|
||
def _generate_fill_pattern(self, fill_name: str, start_offset: float) -> Tuple[List[Dict[str, Any]], List[str]]:
|
||
"""
|
||
Generate fill pattern at specified offset.
|
||
|
||
Returns:
|
||
(notes, roles) - tuple of note list and list of roles used
|
||
"""
|
||
if fill_name not in FILL_PATTERNS:
|
||
return [], []
|
||
|
||
fill = FILL_PATTERNS[fill_name]
|
||
notes = []
|
||
roles_used = []
|
||
|
||
pitch_map = {
|
||
'kick': 36, 'snare': 38, 'hat': 42, 'hat_open': 46,
|
||
'crash': 49, 'ride': 51, 'perc': 50
|
||
}
|
||
|
||
for role, positions in fill['pattern'].items():
|
||
roles_used.append(role)
|
||
pitch = pitch_map.get(role, 50)
|
||
velocity = fill['velocities'].get(role, 90)
|
||
|
||
for pos in positions:
|
||
start = start_offset + pos
|
||
duration = 0.1 if role in ['hat', 'hat_open', 'ride'] else 0.15
|
||
notes.append(self._make_note(pitch, start, duration, velocity))
|
||
|
||
# Track materialization for debugging/logging
|
||
if not hasattr(self, '_transition_materialization_log'):
|
||
self._transition_materialization_log = []
|
||
self._transition_materialization_log.append({
|
||
'fill': fill_name,
|
||
'start': start_offset,
|
||
'notes_count': len(notes),
|
||
'roles': roles_used
|
||
})
|
||
|
||
return notes, roles_used
|
||
|
||
def _generate_transition_events(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
"""Generate fill and transition events between sections."""
|
||
transition_events = []
|
||
|
||
# Calculate start positions for each section
|
||
arrangement_time = 0.0
|
||
for section in sections:
|
||
section['start'] = arrangement_time
|
||
arrangement_time += float(section.get('beats', 0.0) or 0.0)
|
||
|
||
for i, section in enumerate(sections):
|
||
kind = str(section.get('kind', '')).lower()
|
||
start = float(section.get('start', 0.0))
|
||
length = float(section.get('beats', 8.0))
|
||
end = start + length
|
||
|
||
# Check for transition to next section
|
||
if i < len(sections) - 1:
|
||
next_kind = str(sections[i + 1].get('kind', '')).lower()
|
||
transition_key = (kind, next_kind)
|
||
|
||
if transition_key in TRANSITION_EVENTS:
|
||
fills = TRANSITION_EVENTS[transition_key]
|
||
rng = self._section_rng(section, 'transition', salt=20)
|
||
fill_name = rng.choice(fills)
|
||
|
||
# Get notes and roles from fill pattern
|
||
fill_notes, fill_roles = self._generate_fill_pattern(fill_name, end - 2.0)
|
||
|
||
transition_events.append({
|
||
'fill': fill_name,
|
||
'start': end - 2.0,
|
||
'section_kind': kind,
|
||
'next_section_kind': next_kind,
|
||
'roles': fill_roles,
|
||
'notes': fill_notes, # Include actual notes for materialization
|
||
'notes_count': len(fill_notes)
|
||
})
|
||
logger.debug("TRANSITION: Added '%s' at %.1f for %s->%s",
|
||
fill_name, end - 2.0, kind, next_kind)
|
||
|
||
return transition_events
|
||
|
||
def _apply_transition_density_rules(self, transition_events: List[Dict],
|
||
sections: List[Dict]) -> List[Dict]:
|
||
"""
|
||
Apply anti-overcrowding rules to transition events.
|
||
|
||
Returns filtered list of events.
|
||
"""
|
||
if not transition_events:
|
||
return []
|
||
|
||
filtered = []
|
||
last_event_time = {} # Track last time of each event type
|
||
section_fill_counts = defaultdict(int) # Track fills per section
|
||
|
||
for event in transition_events:
|
||
fill_name = event.get('fill', '')
|
||
start = event.get('start', 0.0)
|
||
section_kind = event.get('section_kind', 'drop')
|
||
|
||
# Rule 1: Max fills per section
|
||
max_fills = TRANSITION_DENSITY_RULES['max_fills_by_section'].get(section_kind, 2)
|
||
if section_fill_counts[section_kind] >= max_fills:
|
||
logger.debug("TRANSITION_DENSITY: Skipping '%s' - section '%s' at max (%d fills)",
|
||
fill_name, section_kind, max_fills)
|
||
continue
|
||
|
||
# Rule 2: Minimum distance between same-type events
|
||
min_dist = TRANSITION_DENSITY_RULES['min_distance_same_type'].get(fill_name, 0)
|
||
if fill_name in last_event_time:
|
||
time_since_last = start - last_event_time[fill_name]
|
||
if time_since_last < min_dist:
|
||
logger.debug("TRANSITION_DENSITY: Skipping '%s' - too close to previous (%.1f < %.1f)",
|
||
fill_name, time_since_last, min_dist)
|
||
continue
|
||
|
||
# Rule 3: Check for exclusive events at same position
|
||
skip = False
|
||
for existing in filtered:
|
||
if abs(existing.get('start', -999) - start) < 0.5: # Same position
|
||
for exclusive_set in TRANSITION_DENSITY_RULES['exclusive_events']:
|
||
if fill_name in exclusive_set and existing.get('fill') in exclusive_set:
|
||
logger.debug("TRANSITION_DENSITY: Skipping '%s' - exclusive with '%s' at %.1f",
|
||
fill_name, existing.get('fill'), start)
|
||
skip = True
|
||
break
|
||
if skip:
|
||
break
|
||
|
||
if skip:
|
||
continue
|
||
|
||
# Event passes all rules
|
||
filtered.append(event)
|
||
last_event_time[fill_name] = start
|
||
section_fill_counts[section_kind] += 1
|
||
|
||
logger.info("TRANSITION_DENSITY: %d events passed filtering (from %d original)",
|
||
len(filtered), len(transition_events))
|
||
|
||
return filtered
|
||
|
||
def _transition_events_to_notes(self, transition_events: List[Dict]) -> List[Dict]:
|
||
"""Convert filtered transition events to MIDI notes."""
|
||
notes = []
|
||
for event in transition_events:
|
||
fill_name = event.get('fill', '')
|
||
start = event.get('start', 0.0)
|
||
fill_notes, _ = self._generate_fill_pattern(fill_name, start)
|
||
notes.extend(fill_notes)
|
||
return notes
|
||
|
||
def _materialize_transition_events(self, config: Dict[str, Any],
|
||
track_blueprints: List[Dict]) -> List[Dict]:
|
||
"""
|
||
Materialize transition events into track blueprints.
|
||
|
||
Adds actual MIDI notes to transition-oriented tracks based on transition_events config.
|
||
"""
|
||
transition_events = config.get('transition_events', [])
|
||
if not transition_events:
|
||
config['transition_materialization'] = {
|
||
'events_count': 0,
|
||
'materialized': False,
|
||
'note_count': 0,
|
||
'track_roles': [],
|
||
}
|
||
return track_blueprints
|
||
|
||
transition_track_targets = {
|
||
'drum_fill_4bar': 'snare_fill',
|
||
'drum_fill_2bar': 'snare_fill',
|
||
'snare_roll': 'snare_fill',
|
||
'hat_open_build': 'riser',
|
||
'kick_drop': 'impact',
|
||
'crash_impact': 'crash',
|
||
}
|
||
pitch_to_track_role = {
|
||
36: 'kick',
|
||
38: 'snare_fill',
|
||
42: 'hat_closed',
|
||
46: 'hat_open',
|
||
49: 'crash',
|
||
50: 'perc',
|
||
51: 'ride',
|
||
}
|
||
|
||
# Build a lookup dict of tracks by role
|
||
tracks_by_role = {}
|
||
for track in track_blueprints:
|
||
role = track.get('role', '')
|
||
if role:
|
||
tracks_by_role[role] = track
|
||
|
||
# Track what was materialized
|
||
materialized_count = 0
|
||
materialized_track_roles: set = set()
|
||
|
||
# Materialize each transition event
|
||
for event in transition_events:
|
||
fill_name = event.get('fill', '')
|
||
fill_start = event.get('start', 0.0)
|
||
fill_notes = event.get('notes', [])
|
||
|
||
if not fill_notes:
|
||
event['materialized'] = False
|
||
event['materialized_notes_count'] = 0
|
||
event['materialized_track_roles'] = []
|
||
continue
|
||
|
||
preferred_track_role = transition_track_targets.get(fill_name)
|
||
preferred_note_map: Dict[str, List[Dict[str, Any]]] = {}
|
||
if preferred_track_role and preferred_track_role in tracks_by_role:
|
||
preferred_note_map[preferred_track_role] = list(fill_notes)
|
||
|
||
fallback_note_map: Dict[str, List[Dict[str, Any]]] = {}
|
||
for note in fill_notes:
|
||
note_role = pitch_to_track_role.get(int(note.get('pitch', 0)))
|
||
if note_role:
|
||
fallback_note_map.setdefault(note_role, []).append(note)
|
||
|
||
# Add notes to appropriate tracks
|
||
event_materialized_count = 0
|
||
event_track_roles: set = set()
|
||
|
||
for notes_by_track_role in [preferred_note_map, fallback_note_map]:
|
||
if not notes_by_track_role:
|
||
continue
|
||
|
||
for track_role, notes_to_add in notes_by_track_role.items():
|
||
if track_role not in tracks_by_role:
|
||
logger.debug("TRANSITION_MATERIALIZATION: No track for role '%s', skipping %d notes",
|
||
track_role, len(notes_to_add))
|
||
continue
|
||
if track_role in event_track_roles:
|
||
continue
|
||
|
||
track = tracks_by_role[track_role]
|
||
clips = track.get('clips', [])
|
||
|
||
for clip in clips:
|
||
clip_scene_index = clip.get('scene_index', -1)
|
||
sections = config.get('sections', [])
|
||
if clip_scene_index < 0 or clip_scene_index >= len(sections):
|
||
continue
|
||
|
||
section = sections[clip_scene_index]
|
||
section_start = float(section.get('start', 0.0))
|
||
section_beats = float(section.get('beats', 0.0))
|
||
|
||
if section_start <= fill_start < section_start + section_beats:
|
||
existing_notes = clip.get('notes', [])
|
||
adjusted_notes = []
|
||
for note in notes_to_add:
|
||
adjusted_note = dict(note)
|
||
adjusted_note['start'] = note['start'] - section_start
|
||
adjusted_notes.append(adjusted_note)
|
||
|
||
existing_notes.extend(adjusted_notes)
|
||
existing_notes.sort(key=lambda item: (float(item.get('start', 0.0)), int(item.get('pitch', 0))))
|
||
clip['notes'] = existing_notes
|
||
materialized_count += len(adjusted_notes)
|
||
event_materialized_count += len(adjusted_notes)
|
||
materialized_track_roles.add(track_role)
|
||
event_track_roles.add(track_role)
|
||
|
||
logger.debug("TRANSITION_MATERIALIZATION: Added %d notes to track '%s' (role: %s) for fill '%s' at %.1f",
|
||
len(adjusted_notes), track.get('name', ''), track_role, fill_name, fill_start)
|
||
break
|
||
|
||
if event_materialized_count > 0:
|
||
break
|
||
|
||
event['materialized'] = event_materialized_count > 0
|
||
event['materialized_notes_count'] = event_materialized_count
|
||
event['materialized_track_roles'] = sorted(event_track_roles)
|
||
|
||
logger.info("TRANSITION_MATERIALIZATION: Total %d notes materialized across all tracks", materialized_count)
|
||
config['transition_materialization'] = {
|
||
'events_count': len(transition_events),
|
||
'materialized': materialized_count > 0,
|
||
'note_count': materialized_count,
|
||
'track_roles': sorted(materialized_track_roles),
|
||
}
|
||
return track_blueprints
|
||
|
||
def _find_reference_track_profile(self) -> Optional[Dict[str, Any]]:
|
||
matches: List[Tuple[float, Dict[str, Any]]] = []
|
||
audio_extensions = {'.wav', '.mp3', '.aiff', '.flac', '.aif', '.ogg'}
|
||
for directory in REFERENCE_SEARCH_DIRS:
|
||
if not directory.exists():
|
||
continue
|
||
for candidate in sorted(directory.glob('*')):
|
||
if not candidate.is_file():
|
||
continue
|
||
if candidate.suffix.lower() not in audio_extensions:
|
||
continue
|
||
normalized_name = candidate.name.lower()
|
||
for profile in REFERENCE_TRACK_PROFILES:
|
||
if all(term in normalized_name for term in profile.get('match_terms', [])):
|
||
resolved = dict(profile)
|
||
resolved['path'] = str(candidate)
|
||
resolved['file_name'] = candidate.name
|
||
try:
|
||
modified = float(candidate.stat().st_mtime)
|
||
except Exception:
|
||
modified = 0.0
|
||
matches.append((modified, resolved))
|
||
|
||
if not matches:
|
||
return None
|
||
matches.sort(key=lambda item: item[0], reverse=True)
|
||
return matches[0][1]
|
||
|
||
def _resolve_reference_track_profile(self, genre: str, style: str, bpm: float,
|
||
key: str, structure: str,
|
||
reference_energy_profile: Optional[List[Dict[str, Any]]] = None) -> Optional[Dict[str, Any]]:
|
||
profile = self._find_reference_track_profile()
|
||
if not profile:
|
||
return None
|
||
|
||
target_genre = profile.get('genre', '')
|
||
compatible_genres = {target_genre, 'techno', 'tech-house', 'house'}
|
||
if genre and genre not in compatible_genres:
|
||
return None
|
||
|
||
if bpm <= 0:
|
||
bpm = float(profile.get('bpm', bpm or 0))
|
||
if not key:
|
||
key = profile.get('key', key)
|
||
if not style:
|
||
style = profile.get('style', style)
|
||
if not structure or structure == 'standard':
|
||
structure = profile.get('structure', structure or 'standard')
|
||
|
||
result = {
|
||
'genre': target_genre or genre,
|
||
'style': style,
|
||
'bpm': bpm,
|
||
'key': key,
|
||
'structure': structure,
|
||
'reference': profile,
|
||
}
|
||
|
||
# Forward energy profile if available
|
||
if reference_energy_profile:
|
||
result['reference_energy_profile'] = reference_energy_profile
|
||
|
||
return result
|
||
|
||
def _build_return_states(self, returns: List[Dict[str, Any]], section: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
if not returns:
|
||
return []
|
||
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
energy = max(1, int(section.get('energy', 1)))
|
||
profile_name = str(self._current_generation_profile.get('name', 'default')).lower()
|
||
style_text = str(self._current_generation_profile.get('style_text', '')).lower()
|
||
|
||
volume_factors = {
|
||
'space': {'intro': 0.94, 'build': 0.84, 'drop': 0.7, 'break': 1.02, 'outro': 0.86},
|
||
'echo': {'intro': 0.8, 'build': 1.04, 'drop': 0.72, 'break': 0.92, 'outro': 0.78},
|
||
'heat': {'intro': 0.56, 'build': 0.88, 'drop': 1.06, 'break': 0.42, 'outro': 0.66},
|
||
'glue': {'intro': 0.72, 'build': 0.86, 'drop': 1.02, 'break': 0.58, 'outro': 0.74},
|
||
}
|
||
space_mix = {'intro': 0.94, 'build': 0.88, 'drop': 0.8, 'break': 1.0, 'outro': 0.9}
|
||
echo_mix = {'intro': 0.72, 'build': 0.92, 'drop': 0.62, 'break': 0.84, 'outro': 0.76}
|
||
width_targets = {'intro': 1.02, 'build': 1.08, 'drop': 1.12, 'break': 1.16, 'outro': 1.04}
|
||
filter_factors = {'intro': 0.86, 'build': 1.0, 'drop': 1.18, 'break': 0.78, 'outro': 0.9}
|
||
drive_offsets = {'intro': -1.2, 'build': 0.2, 'drop': 1.0, 'break': -1.6, 'outro': -0.5}
|
||
threshold_offsets = {'intro': 1.5, 'build': -0.5, 'drop': -2.0, 'break': 2.5, 'outro': 1.0}
|
||
|
||
states = []
|
||
for return_index, return_spec in enumerate(returns):
|
||
send_key = str(return_spec.get('send_key', return_spec.get('name', ''))).strip().lower()
|
||
if not send_key:
|
||
continue
|
||
|
||
base_volume = float(return_spec.get('volume', 0.7))
|
||
volume_factor = volume_factors.get(send_key, {}).get(kind, 1.0)
|
||
if send_key in ['heat', 'glue'] and energy >= 4:
|
||
volume_factor += 0.04
|
||
elif send_key in ['space', 'echo'] and kind == 'break':
|
||
volume_factor += 0.04
|
||
|
||
if profile_name == 'warehouse' and send_key == 'heat':
|
||
volume_factor += 0.05
|
||
elif profile_name == 'festival' and send_key == 'space':
|
||
volume_factor += 0.06
|
||
elif profile_name == 'swing' and send_key == 'echo':
|
||
volume_factor += 0.05
|
||
elif profile_name == 'jackin' and send_key == 'glue':
|
||
volume_factor += 0.05
|
||
|
||
if 'industrial' in style_text and send_key == 'heat':
|
||
volume_factor += 0.05
|
||
if 'latin' in style_text and send_key == 'echo':
|
||
volume_factor += 0.06
|
||
|
||
state = {
|
||
'return_index': return_index,
|
||
'send_key': send_key,
|
||
'volume': self._clamp_unit(base_volume * volume_factor),
|
||
'device_parameters': [],
|
||
}
|
||
|
||
for device_index, device_spec in enumerate(return_spec.get('device_chain', []) or []):
|
||
if not isinstance(device_spec, dict):
|
||
continue
|
||
device_name = str(device_spec.get('device', '') or '').strip()
|
||
if not device_name:
|
||
continue
|
||
device_name_lower = device_name.lower()
|
||
base_parameters = dict(device_spec.get('parameters', {}))
|
||
parameter_updates = {}
|
||
|
||
if send_key == 'space':
|
||
if 'hybrid reverb' in device_name_lower:
|
||
parameter_updates['Dry/Wet'] = space_mix.get(kind, 0.9)
|
||
elif 'auto filter' in device_name_lower:
|
||
base_frequency = float(base_parameters.get('Frequency', 8200.0) or 8200.0)
|
||
parameter_updates['Frequency'] = round(base_frequency * filter_factors.get(kind, 1.0), 3)
|
||
parameter_updates['Dry/Wet'] = {'intro': 0.18, 'build': 0.22, 'drop': 0.08, 'break': 0.28, 'outro': 0.14}.get(kind, 0.16)
|
||
elif 'utility' in device_name_lower:
|
||
parameter_updates['Stereo Width'] = width_targets.get(kind, 1.08)
|
||
elif send_key == 'echo':
|
||
if 'echo' in device_name_lower:
|
||
parameter_updates['Dry/Wet'] = echo_mix.get(kind, 0.78)
|
||
elif 'auto filter' in device_name_lower:
|
||
base_frequency = float(base_parameters.get('Frequency', 9800.0) or 9800.0)
|
||
parameter_updates['Frequency'] = round(base_frequency * {'intro': 0.94, 'build': 1.08, 'drop': 0.88, 'break': 0.9, 'outro': 0.92}.get(kind, 1.0), 3)
|
||
parameter_updates['Dry/Wet'] = {'intro': 0.08, 'build': 0.14, 'drop': 0.06, 'break': 0.16, 'outro': 0.09}.get(kind, 0.1)
|
||
elif 'hybrid reverb' in device_name_lower:
|
||
parameter_updates['Dry/Wet'] = {'intro': 0.12, 'build': 0.18, 'drop': 0.08, 'break': 0.22, 'outro': 0.1}.get(kind, 0.12)
|
||
elif send_key == 'heat':
|
||
if 'saturator' in device_name_lower:
|
||
base_drive = float(base_parameters.get('Drive', 4.5) or 4.5)
|
||
parameter_updates['Drive'] = round(max(0.5, base_drive + drive_offsets.get(kind, 0.0)), 3)
|
||
elif 'compressor' in device_name_lower:
|
||
base_threshold = float(base_parameters.get('Threshold', -16.0) or -16.0)
|
||
parameter_updates['Threshold'] = round(base_threshold + threshold_offsets.get(kind, 0.0), 3)
|
||
elif send_key == 'glue':
|
||
if 'compressor' in device_name_lower:
|
||
base_threshold = float(base_parameters.get('Threshold', -18.0) or -18.0)
|
||
parameter_updates['Threshold'] = round(base_threshold + {'intro': 1.0, 'build': -0.6, 'drop': -1.4, 'break': 1.8, 'outro': 0.8}.get(kind, 0.0), 3)
|
||
elif 'limiter' in device_name_lower:
|
||
parameter_updates['Gain'] = {'intro': -0.4, 'build': 0.0, 'drop': 0.35, 'break': -0.6, 'outro': -0.3}.get(kind, 0.0)
|
||
|
||
for parameter_name, value in parameter_updates.items():
|
||
state['device_parameters'].append({
|
||
'device_index': int(device_index),
|
||
'device_name': device_name,
|
||
'parameter': parameter_name,
|
||
'value': value,
|
||
})
|
||
|
||
states.append(state)
|
||
|
||
return states
|
||
|
||
# =========================================================================
|
||
# SECTION AUTOMATION METHODS
|
||
# =========================================================================
|
||
|
||
def _generate_automation_envelope(
|
||
self,
|
||
parameter_start: float,
|
||
parameter_end: float,
|
||
section_length: float,
|
||
curve_name: str = 'linear',
|
||
num_points: int = 8
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Generate automation envelope points for a parameter over a section.
|
||
|
||
Args:
|
||
parameter_start: Starting value of the parameter
|
||
parameter_end: Ending value of the parameter
|
||
section_length: Length of the section in beats
|
||
curve_name: Name of the envelope curve to use
|
||
num_points: Number of envelope points to generate
|
||
|
||
Returns:
|
||
List of automation points with time and value
|
||
"""
|
||
curve_func = ENVELOPE_CURVES.get(curve_name, ENVELOPE_CURVES['linear'])
|
||
envelope_points = []
|
||
|
||
for i in range(num_points):
|
||
position = i / (num_points - 1) if num_points > 1 else 0.0
|
||
curved_position = curve_func(position)
|
||
value = parameter_start + (parameter_end - parameter_start) * curved_position
|
||
time = section_length * position
|
||
|
||
envelope_points.append({
|
||
'time': round(time, 3),
|
||
'value': round(value, 4),
|
||
'curve_position': round(position, 3),
|
||
})
|
||
|
||
return envelope_points
|
||
|
||
def _build_section_automation(
|
||
self,
|
||
section: Dict[str, Any],
|
||
buses: List[Dict[str, Any]],
|
||
returns: List[Dict[str, Any]]
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
Build automation data for a single section.
|
||
|
||
Args:
|
||
section: Section configuration dictionary
|
||
buses: List of bus track configurations
|
||
returns: List of return track configurations
|
||
|
||
Returns:
|
||
Dictionary containing automation data for the section
|
||
"""
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
section_length = float(section.get('beats', 32.0))
|
||
energy = float(section.get('energy', 1))
|
||
|
||
# Get base automation template for this section kind
|
||
base_automation = SECTION_AUTOMATION.get(kind, SECTION_AUTOMATION.get('drop', {}))
|
||
|
||
# Determine envelope curve
|
||
curve_name = base_automation.get('envelope_curve', 'linear')
|
||
|
||
# Apply energy scaling
|
||
energy_factor = max(0.5, min(1.5, energy / 3.0))
|
||
|
||
automation_data = {
|
||
'section_index': int(section.get('index', 0)),
|
||
'section_name': section.get('name', 'SECTION'),
|
||
'section_kind': kind,
|
||
'section_length': section_length,
|
||
'energy': round(base_automation.get('energy', 0.5) * energy_factor, 3),
|
||
'bus_automation': [],
|
||
'return_automation': [],
|
||
'master_automation': {},
|
||
}
|
||
|
||
# Build bus automation
|
||
for bus in buses:
|
||
bus_key = str(bus.get('key', '')).lower()
|
||
if not bus_key:
|
||
continue
|
||
|
||
bus_filter_settings = base_automation.get('filters', {}).get(bus_key, {})
|
||
if not bus_filter_settings:
|
||
continue
|
||
|
||
bus_auto = {
|
||
'bus_key': bus_key,
|
||
'bus_name': bus.get('name', bus_key.upper()),
|
||
'parameters': []
|
||
}
|
||
|
||
# Filter frequency automation
|
||
if 'frequency' in bus_filter_settings:
|
||
freq_start = bus_filter_settings['frequency'] * (1.1 - energy_factor * 0.2)
|
||
freq_end = bus_filter_settings['frequency'] * energy_factor
|
||
bus_auto['parameters'].append({
|
||
'device': 'Auto Filter',
|
||
'parameter': 'Frequency',
|
||
'envelope': self._generate_automation_envelope(
|
||
freq_start, freq_end, section_length, curve_name
|
||
),
|
||
'start_value': round(freq_start, 1),
|
||
'end_value': round(freq_end, 1),
|
||
})
|
||
|
||
# Filter resonance automation
|
||
if 'resonance' in bus_filter_settings:
|
||
res_start = bus_filter_settings['resonance'] * 0.8
|
||
res_end = bus_filter_settings['resonance'] * energy_factor
|
||
bus_auto['parameters'].append({
|
||
'device': 'Auto Filter',
|
||
'parameter': 'Resonance',
|
||
'envelope': self._generate_automation_envelope(
|
||
res_start, res_end, section_length, 'ease_in_out'
|
||
),
|
||
'start_value': round(res_start, 3),
|
||
'end_value': round(res_end, 3),
|
||
})
|
||
|
||
if bus_auto['parameters']:
|
||
automation_data['bus_automation'].append(bus_auto)
|
||
|
||
# Build return automation
|
||
reverb_settings = base_automation.get('reverb', {})
|
||
delay_settings = base_automation.get('delay', {})
|
||
compression_settings = base_automation.get('compression', {})
|
||
saturation_settings = base_automation.get('saturation', {})
|
||
stereo_width_settings = base_automation.get('stereo_width', {})
|
||
|
||
for return_track in returns:
|
||
send_key = str(return_track.get('send_key', '')).lower()
|
||
if not send_key:
|
||
continue
|
||
|
||
return_auto = {
|
||
'send_key': send_key,
|
||
'return_name': return_track.get('name', send_key.upper()),
|
||
'parameters': []
|
||
}
|
||
|
||
if send_key == 'space' and reverb_settings:
|
||
# Reverb send level
|
||
return_auto['parameters'].append({
|
||
'device': 'Hybrid Reverb',
|
||
'parameter': 'Dry/Wet',
|
||
'envelope': self._generate_automation_envelope(
|
||
reverb_settings.get('send_level', 0.2) * 0.9,
|
||
reverb_settings.get('send_level', 0.2) * energy_factor,
|
||
section_length, curve_name
|
||
),
|
||
'start_value': round(reverb_settings.get('send_level', 0.2) * 0.9, 3),
|
||
'end_value': round(reverb_settings.get('send_level', 0.2) * energy_factor, 3),
|
||
})
|
||
# Decay time
|
||
return_auto['parameters'].append({
|
||
'device': 'Hybrid Reverb',
|
||
'parameter': 'Decay Time',
|
||
'envelope': self._generate_automation_envelope(
|
||
reverb_settings.get('decay_time', 2.0) * 0.85,
|
||
reverb_settings.get('decay_time', 2.0),
|
||
section_length, 'ease_out'
|
||
),
|
||
'start_value': round(reverb_settings.get('decay_time', 2.0) * 0.85, 2),
|
||
'end_value': round(reverb_settings.get('decay_time', 2.0), 2),
|
||
})
|
||
|
||
elif send_key == 'echo' and delay_settings:
|
||
# Delay send level
|
||
return_auto['parameters'].append({
|
||
'device': 'Echo',
|
||
'parameter': 'Dry/Wet',
|
||
'envelope': self._generate_automation_envelope(
|
||
delay_settings.get('send_level', 0.15) * 0.85,
|
||
delay_settings.get('send_level', 0.15) * energy_factor,
|
||
section_length, curve_name
|
||
),
|
||
'start_value': round(delay_settings.get('send_level', 0.15) * 0.85, 3),
|
||
'end_value': round(delay_settings.get('send_level', 0.15) * energy_factor, 3),
|
||
})
|
||
# Feedback
|
||
return_auto['parameters'].append({
|
||
'device': 'Echo',
|
||
'parameter': 'Feedback',
|
||
'envelope': self._generate_automation_envelope(
|
||
delay_settings.get('feedback', 0.3) * 0.8,
|
||
delay_settings.get('feedback', 0.3),
|
||
section_length, 'ramp_up'
|
||
),
|
||
'start_value': round(delay_settings.get('feedback', 0.3) * 0.8, 3),
|
||
'end_value': round(delay_settings.get('feedback', 0.3), 3),
|
||
})
|
||
|
||
elif send_key == 'heat' and saturation_settings:
|
||
# Saturation drive
|
||
return_auto['parameters'].append({
|
||
'device': 'Saturator',
|
||
'parameter': 'Drive',
|
||
'envelope': self._generate_automation_envelope(
|
||
saturation_settings.get('drive', 2.0) * 0.6,
|
||
saturation_settings.get('drive', 2.0) * energy_factor,
|
||
section_length, 'ramp_up'
|
||
),
|
||
'start_value': round(saturation_settings.get('drive', 2.0) * 0.6, 2),
|
||
'end_value': round(saturation_settings.get('drive', 2.0) * energy_factor, 2),
|
||
})
|
||
|
||
elif send_key == 'glue' and compression_settings:
|
||
# Compressor threshold
|
||
return_auto['parameters'].append({
|
||
'device': 'Compressor',
|
||
'parameter': 'Threshold',
|
||
'envelope': self._generate_automation_envelope(
|
||
compression_settings.get('threshold', -12.0) + 3,
|
||
compression_settings.get('threshold', -12.0) - (energy_factor - 1) * 2,
|
||
section_length, 'ease_in'
|
||
),
|
||
'start_value': round(compression_settings.get('threshold', -12.0) + 3, 1),
|
||
'end_value': round(compression_settings.get('threshold', -12.0) - (energy_factor - 1) * 2, 1),
|
||
})
|
||
|
||
if return_auto['parameters']:
|
||
automation_data['return_automation'].append(return_auto)
|
||
|
||
# Build master automation
|
||
automation_data['master_automation'] = {
|
||
'stereo_width': {
|
||
'parameter': 'Stereo Width',
|
||
'envelope': self._generate_automation_envelope(
|
||
stereo_width_settings.get('value', 1.0) * 0.9,
|
||
stereo_width_settings.get('value', 1.0),
|
||
section_length, 'ease_in_out'
|
||
),
|
||
'start_value': round(stereo_width_settings.get('value', 1.0) * 0.9, 3),
|
||
'end_value': round(stereo_width_settings.get('value', 1.0), 3),
|
||
},
|
||
'compression': {
|
||
'parameter': 'Ratio',
|
||
'envelope': self._generate_automation_envelope(
|
||
compression_settings.get('ratio', 2.0) * 0.8,
|
||
compression_settings.get('ratio', 2.0) * energy_factor,
|
||
section_length, 'ease_in'
|
||
),
|
||
'start_value': round(compression_settings.get('ratio', 2.0) * 0.8, 2),
|
||
'end_value': round(compression_settings.get('ratio', 2.0) * energy_factor, 2),
|
||
},
|
||
}
|
||
|
||
return automation_data
|
||
|
||
def _build_full_automation_blueprint(
|
||
self,
|
||
sections: List[Dict[str, Any]],
|
||
buses: List[Dict[str, Any]],
|
||
returns: List[Dict[str, Any]]
|
||
) -> List[Dict[str, Any]]:
|
||
"""
|
||
Build complete automation blueprint for all sections.
|
||
|
||
Args:
|
||
sections: List of section configurations
|
||
buses: List of bus track configurations
|
||
returns: List of return track configurations
|
||
|
||
Returns:
|
||
List of automation data dictionaries, one per section
|
||
"""
|
||
automation_blueprint = []
|
||
|
||
for section in sections:
|
||
section_automation = self._build_section_automation(section, buses, returns)
|
||
automation_blueprint.append(section_automation)
|
||
|
||
return automation_blueprint
|
||
|
||
def _build_master_state(self, section_kind: str) -> Dict[str, Any]:
|
||
"""
|
||
Build master chain state for a section.
|
||
|
||
Returns a snapshot payload with flat device parameters for master chain.
|
||
"""
|
||
section = section_kind.lower()
|
||
device_parameters = []
|
||
for device_name, parameter_map in MASTER_DEVICE_AUTOMATION.items():
|
||
for parameter_name, section_values in parameter_map.items():
|
||
value = section_values.get(section, section_values.get('drop', 0.0))
|
||
clamp = MASTER_SAFETY_CLAMPS.get(parameter_name)
|
||
if clamp:
|
||
value = max(clamp['min'], min(clamp['max'], float(value)))
|
||
device_parameters.append({
|
||
'device_name': device_name,
|
||
'parameter': parameter_name,
|
||
'value': round(float(value), 3),
|
||
})
|
||
|
||
return {
|
||
'section': section,
|
||
'device_parameters': device_parameters,
|
||
}
|
||
|
||
def _build_device_parameters_for_role(self, role: str, section_kind: str) -> List[Dict[str, Any]]:
|
||
"""
|
||
Build flat device parameter automation entries for a track role in a section.
|
||
"""
|
||
role_lower = role.lower().replace(' ', '_').replace('-', '_')
|
||
if role_lower not in SECTION_DEVICE_AUTOMATION:
|
||
return []
|
||
section = section_kind.lower()
|
||
device_params = []
|
||
for device_name, parameter_map in SECTION_DEVICE_AUTOMATION.get(role_lower, {}).items():
|
||
for parameter_name, section_values in parameter_map.items():
|
||
value = section_values.get(section, section_values.get('drop', 0.0))
|
||
clamp = DEVICE_PARAMETER_SAFETY_CLAMPS.get(parameter_name)
|
||
if clamp:
|
||
value = max(clamp['min'], min(clamp['max'], float(value)))
|
||
device_params.append({
|
||
'device_name': device_name,
|
||
'parameter': parameter_name,
|
||
'value': round(float(value), 3),
|
||
})
|
||
return device_params
|
||
|
||
def _build_bus_device_parameters(self, bus_key: str, section_kind: str) -> List[Dict[str, Any]]:
|
||
"""
|
||
Build flat device parameter automation entries for a bus track in a section.
|
||
Uses BUS_DEVICE_AUTOMATION constant for per-section values.
|
||
"""
|
||
bus_key_lower = bus_key.lower()
|
||
if bus_key_lower not in BUS_DEVICE_AUTOMATION:
|
||
return []
|
||
section = section_kind.lower()
|
||
device_params = []
|
||
for device_name, parameter_map in BUS_DEVICE_AUTOMATION.get(bus_key_lower, {}).items():
|
||
for parameter_name, section_values in parameter_map.items():
|
||
value = section_values.get(section, section_values.get('drop',0.0))
|
||
clamp = DEVICE_PARAMETER_SAFETY_CLAMPS.get(parameter_name)
|
||
if clamp:
|
||
value = max(clamp['min'], min(clamp['max'], float(value)))
|
||
device_params.append({
|
||
'device_name': device_name,
|
||
'parameter': parameter_name,
|
||
'value': round(float(value), 3),
|
||
})
|
||
return device_params
|
||
|
||
def _build_performance_snapshots(self, blueprint_tracks: List[Dict[str, Any]],
|
||
sections: List[Dict[str, Any]],
|
||
returns: Optional[List[Dict[str, Any]]] = None,
|
||
buses: Optional[List[Dict[str, Any]]] = None,
|
||
reference_energy_profile: Optional[List[Dict[str, Any]]] = None) -> List[Dict[str, Any]]:
|
||
performance = []
|
||
stereo_roles = {'hat_closed', 'hat_open', 'top_loop', 'perc', 'ride', 'pad', 'pluck', 'arp', 'counter', 'reverse_fx', 'riser', 'impact', 'atmos', 'vocal'}
|
||
profile_pan_width = float(self._current_generation_profile.get('pan_width', 0.12))
|
||
volume_factors = {
|
||
'intro': 0.86,
|
||
'build': 0.94,
|
||
'drop': 1.02,
|
||
'break': 0.78,
|
||
'outro': 0.8,
|
||
}
|
||
|
||
# Build energy profile lookup by section index for adaptive mixing
|
||
energy_by_index = {}
|
||
if reference_energy_profile:
|
||
for i, ep in enumerate(reference_energy_profile):
|
||
energy_by_index[i] = ep.get('energy_mean', 0.5)
|
||
else:
|
||
# Fallback: use section features if available
|
||
for i, section in enumerate(sections):
|
||
features = section.get('features', {})
|
||
energy_by_index[i] = features.get('energy_mean', features.get('energy', 0.5))
|
||
|
||
space_send_factors = {
|
||
'intro': 1.15,
|
||
'build': 1.0,
|
||
'drop': 0.82,
|
||
'break': 1.35,
|
||
'outro': 1.05,
|
||
}
|
||
echo_send_factors = {
|
||
'intro': 1.08,
|
||
'build': 1.18,
|
||
'drop': 0.78,
|
||
'break': 1.45,
|
||
'outro': 0.95,
|
||
}
|
||
heat_send_factors = {
|
||
'intro': 0.55,
|
||
'build': 0.92,
|
||
'drop': 1.18,
|
||
'break': 0.42,
|
||
'outro': 0.72,
|
||
}
|
||
glue_send_factors = {
|
||
'intro': 0.72,
|
||
'build': 0.96,
|
||
'drop': 1.08,
|
||
'break': 0.58,
|
||
'outro': 0.78,
|
||
}
|
||
|
||
for section_idx, section in enumerate(sections):
|
||
kind = str(section.get('kind', 'drop')).lower()
|
||
energy = max(1, int(section.get('energy', 1)))
|
||
|
||
# Get energy_mean from reference profile for adaptive volume scaling
|
||
ref_energy_mean = energy_by_index.get(section_idx, 0.5)
|
||
|
||
snapshot = {
|
||
'scene_index': int(section.get('index', len(performance))),
|
||
'name': section.get('name', "SECTION"),
|
||
'track_states': [],
|
||
'return_states': self._build_return_states(list(returns or []), section),
|
||
'bus_states': [],
|
||
}
|
||
|
||
for track_index, track_data in enumerate(blueprint_tracks):
|
||
role = track_data.get('role', '')
|
||
base_volume = float(track_data.get('volume', 0.72))
|
||
base_pan = float(track_data.get('pan', 0.0))
|
||
base_sends = dict(track_data.get('sends', {}))
|
||
intensity = self._role_intensity(role, section)
|
||
is_muted = role != 'sc_trigger' and intensity <= 0
|
||
|
||
if is_muted:
|
||
target_volume = round(base_volume * 0.08, 3)
|
||
else:
|
||
factor = volume_factors.get(kind, 1.0) + max(0.0, (energy - 3) * 0.03)
|
||
if role in ['kick', 'sub_bass', 'bass'] and kind == 'drop':
|
||
factor += 0.04
|
||
if role in ['pad', 'atmos', 'drone'] and kind == 'break':
|
||
factor += 0.08
|
||
if role in ['reverse_fx', 'riser', 'impact'] and kind in ['build', 'break']:
|
||
factor += 0.06 * float(self._current_generation_profile.get('fx_bias', 1.0))
|
||
|
||
# Apply energy-based volume scaling from reference profile
|
||
if ref_energy_mean < 0.3:
|
||
# Quiet sections (intro, quiet breaks) - reduce volume
|
||
energy_volume_factor = 0.85
|
||
elif ref_energy_mean > 0.7:
|
||
# High energy sections (drops, peaks) - boost volume
|
||
energy_volume_factor = 1.08
|
||
else:
|
||
energy_volume_factor = 1.0
|
||
|
||
target_volume = round(min(1.0, max(0.0, base_volume * factor * energy_volume_factor)), 3)
|
||
|
||
target_pan = base_pan
|
||
pan_variant = str(section.get('pan_variant', 'narrow')).lower()
|
||
if role in stereo_roles:
|
||
if pan_variant == 'tilt_left':
|
||
direction = -1.0
|
||
width = profile_pan_width
|
||
elif pan_variant == 'tilt_right':
|
||
direction = 1.0
|
||
width = profile_pan_width
|
||
elif pan_variant == 'wide':
|
||
direction = -1.0 if track_index % 2 == 0 else 1.0
|
||
width = profile_pan_width * 1.1
|
||
else:
|
||
direction = -1.0 if track_index % 2 == 0 else 1.0
|
||
width = profile_pan_width * 0.55
|
||
|
||
if kind == 'break':
|
||
width *= 1.18
|
||
elif kind == 'drop':
|
||
width *= 0.92
|
||
target_pan = self._clamp_pan(base_pan + (direction * width))
|
||
|
||
target_sends = {}
|
||
for send_name, send_value in base_sends.items():
|
||
send_factor = 1.0
|
||
if send_name == 'space':
|
||
send_factor = space_send_factors.get(kind, 1.0)
|
||
elif send_name == 'echo':
|
||
send_factor = echo_send_factors.get(kind, 1.0)
|
||
elif send_name == 'heat':
|
||
send_factor = heat_send_factors.get(kind, 1.0)
|
||
elif send_name == 'glue':
|
||
send_factor = glue_send_factors.get(kind, 1.0)
|
||
|
||
if role in ['riser', 'impact'] and kind in ['build', 'break']:
|
||
send_factor += 0.18
|
||
if role == 'vocal' and kind in ['build', 'drop']:
|
||
send_factor += 0.12
|
||
if role in ['kick', 'sub_bass', 'bass'] and send_name in ['heat', 'glue'] and kind == 'drop':
|
||
send_factor += 0.1
|
||
if is_muted:
|
||
send_factor *= 0.25
|
||
|
||
target_sends[send_name] = round(min(1.0, max(0.0, float(send_value) * send_factor)), 3)
|
||
|
||
track_state = {
|
||
'track_index': track_index,
|
||
'role': role,
|
||
'mute': is_muted,
|
||
'volume': target_volume,
|
||
'pan': target_pan,
|
||
'sends': target_sends,
|
||
}
|
||
|
||
# Add device_parameters to track state
|
||
device_params = self._build_device_parameters_for_role(role, kind)
|
||
if device_params:
|
||
track_state['device_parameters'] = device_params
|
||
|
||
snapshot['track_states'].append(track_state)
|
||
|
||
# Add bus states to snapshot
|
||
for bus_data in list(buses or []):
|
||
bus_key = str(bus_data.get('key', '')).lower()
|
||
if not bus_key:
|
||
continue
|
||
bus_device_params = self._build_bus_device_parameters(bus_key, kind)
|
||
if bus_device_params:
|
||
bus_state = {
|
||
'bus_key': bus_key,
|
||
'bus_name': bus_data.get('name', bus_key.upper()),
|
||
'device_parameters': bus_device_params,
|
||
}
|
||
snapshot['bus_states'].append(bus_state)
|
||
|
||
# Add master state to snapshot
|
||
master_state = self._build_master_state(kind)
|
||
if master_state.get('device_parameters'):
|
||
snapshot['master_state'] = master_state
|
||
|
||
performance.append(snapshot)
|
||
|
||
return performance
|
||
|
||
def _build_mix_automation_summary(self, performance: List[Dict]) -> Dict[str, Any]:
|
||
"""
|
||
Build summary of automation in performance snapshots.
|
||
|
||
Returns:
|
||
- track_snapshots_with_device_automation: count
|
||
- return_snapshots_with_device_automation: count
|
||
- bus_snapshots_with_device_automation: count
|
||
- master_snapshots_count: count
|
||
- track_roles_touched: list of roles with device automation
|
||
- bus_keys_touched: list of bus keys with device automation
|
||
- master_parameters_touched: list of master params automated
|
||
"""
|
||
track_count = 0
|
||
return_count = 0
|
||
bus_count = 0
|
||
master_count = 0
|
||
track_roles = set()
|
||
bus_keys = set()
|
||
master_params = set()
|
||
|
||
for snapshot in performance:
|
||
# Check track states
|
||
for track_state in snapshot.get('track_states', []):
|
||
if 'device_parameters' in track_state and track_state['device_parameters']:
|
||
track_count += 1
|
||
role = track_state.get('role', 'unknown')
|
||
track_roles.add(role)
|
||
|
||
# Check return states
|
||
for return_state in snapshot.get('return_states', []):
|
||
if 'device_parameters' in return_state and return_state['device_parameters']:
|
||
return_count += 1
|
||
|
||
# Check bus states
|
||
for bus_state in snapshot.get('bus_states', []):
|
||
if 'device_parameters' in bus_state and bus_state['device_parameters']:
|
||
bus_count += 1
|
||
bus_key = bus_state.get('bus_key', 'unknown')
|
||
bus_keys.add(bus_key)
|
||
|
||
# Check master state
|
||
master_state = snapshot.get('master_state', {})
|
||
if master_state.get('device_parameters'):
|
||
master_count += 1
|
||
for item in master_state.get('device_parameters', []):
|
||
param_name = str(item.get('parameter', '') or '').strip()
|
||
if param_name:
|
||
master_params.add(param_name)
|
||
|
||
return {
|
||
'track_snapshots_with_device_automation': track_count,
|
||
'return_snapshots_with_device_automation': return_count,
|
||
'bus_snapshots_with_device_automation': bus_count,
|
||
'master_snapshots_count': master_count,
|
||
'track_roles_touched': sorted(list(track_roles)),
|
||
'bus_keys_touched': sorted(list(bus_keys)),
|
||
'master_parameters_touched': sorted(list(master_params))
|
||
}
|
||
|
||
def _verify_automation_safety(self, performance: List[Dict]) -> List[str]:
|
||
"""
|
||
Verify automation values are within safe ranges.
|
||
|
||
Returns list of warnings if any values are outside safe ranges.
|
||
"""
|
||
warnings = []
|
||
|
||
for i, snapshot in enumerate(performance):
|
||
# Check master state
|
||
master_state = snapshot.get('master_state', {})
|
||
for item in master_state.get('device_parameters', []):
|
||
device_name = str(item.get('device_name', 'unknown'))
|
||
param_name = str(item.get('parameter', '') or '').strip()
|
||
value = float(item.get('value', 0.0))
|
||
clamp = MASTER_SAFETY_CLAMPS.get(param_name)
|
||
if clamp and (value < clamp['min'] or value > clamp['max']):
|
||
warnings.append(f"Snapshot {i}: {device_name}.{param_name}={value} outside safe range [{clamp['min']}, {clamp['max']}]")
|
||
|
||
return warnings
|
||
|
||
def _build_gain_staging_summary(self, config: Dict[str, Any]) -> Dict[str, Any]:
|
||
"""
|
||
Build gain staging summary for the generated config.
|
||
"""
|
||
warnings = []
|
||
|
||
# Check bus volumes for extreme values
|
||
bus_volumes = self._calibrated_bus_volumes or {}
|
||
for bus_name, vol in bus_volumes.items():
|
||
if vol > 0.9:
|
||
warnings.append(f"Bus {bus_name} volume > 0.9: {vol:.3f}")
|
||
|
||
# Check master limiter gain
|
||
master = config.get('master', {})
|
||
master_limiter_gain = 0.0
|
||
for device in master.get('device_chain', []):
|
||
if device.get('device') == 'Limiter':
|
||
master_limiter_gain = device.get('parameters', {}).get('Gain', 0.0)
|
||
if master_limiter_gain > 1.0:
|
||
warnings.append(f"Master limiter gain > 1.0: {master_limiter_gain:.3f}")
|
||
|
||
# Check track volumes
|
||
for track in config.get('tracks', []):
|
||
vol = track.get('volume', 0.0)
|
||
role = track.get('role', 'unknown')
|
||
if vol > 0.9:
|
||
warnings.append(f"Track {role} volume > 0.9: {vol:.3f}")
|
||
|
||
return {
|
||
'master_profile_used': getattr(self, '_master_profile_used', 'default'),
|
||
'style_adjustments_applied': getattr(self, '_style_adjustments_applied', []),
|
||
'bus_volumes': bus_volumes,
|
||
'track_volume_overrides_count': getattr(self, '_gain_calibration_overrides_count', 0),
|
||
'peak_reductions_applied_count': getattr(self, '_peak_reductions_count', 0),
|
||
'headroom_target_db': TARGET_HEADROOM_DB,
|
||
'warnings': warnings,
|
||
}
|
||
|
||
def generate_config(self, genre: str, style: str = "", bpm: float = 0,
|
||
key: str = "", structure: str = "standard",
|
||
palette: Optional[Dict[str, str]] = None) -> Dict[str, Any]:
|
||
"""
|
||
Genera una configuración completa de track
|
||
|
||
Args:
|
||
genre: Género musical
|
||
style: Sub-estilo
|
||
bpm: BPM (0 = auto)
|
||
key: Tonalidad ("" = auto)
|
||
structure: Tipo de estructura
|
||
"""
|
||
genre = genre.lower().replace(' ', '-')
|
||
style = style.lower() if style else ""
|
||
variant_seed = random.SystemRandom().randint(1000, 999999)
|
||
random.seed(variant_seed)
|
||
|
||
# Decay pattern variant memory to allow reuse
|
||
_decay_pattern_variant_memory()
|
||
|
||
# Reset gain staging counters
|
||
self._gain_calibration_overrides_count = 0
|
||
self._peak_reductions_count = 0
|
||
self._style_adjustments_applied = []
|
||
self._calibrated_bus_volumes = {}
|
||
self._master_profile_used = 'default'
|
||
|
||
reference_resolution = self._resolve_reference_track_profile(genre, style, bpm, key, structure)
|
||
if reference_resolution:
|
||
genre = reference_resolution.get('genre', genre) or genre
|
||
style = reference_resolution.get('style', style)
|
||
bpm = float(reference_resolution.get('bpm', bpm or 0))
|
||
key = reference_resolution.get('key', key)
|
||
structure = reference_resolution.get('structure', structure)
|
||
|
||
# Obtener configuración del género
|
||
genre_config = GENRE_CONFIGS.get(genre, GENRE_CONFIGS['techno'])
|
||
|
||
# Determinar BPM
|
||
if bpm <= 0:
|
||
bpm = genre_config['default_bpm']
|
||
|
||
# Determinar key
|
||
if not key:
|
||
key = random.choice(genre_config['keys'])
|
||
|
||
# Determinar estilo si no se especificó
|
||
if not style:
|
||
style = random.choice(genre_config['styles'])
|
||
|
||
# Parsear key
|
||
_root_note = key[:-1] if len(key) > 1 else key # noqa: F841 - parsed when needed per section
|
||
is_minor = 'm' in key.lower()
|
||
scale = 'minor' if is_minor else 'major'
|
||
profile = self._build_arrangement_profile(genre, style, variant_seed)
|
||
profile['style_text'] = f"{genre} {style}".strip().lower()
|
||
profile['reference_name'] = str(((reference_resolution or {}).get('reference') or {}).get('name', '')).lower()
|
||
self._current_generation_profile = profile
|
||
sections = self._build_sections(structure, style, variant_seed, profile)
|
||
|
||
# Crear configuración base
|
||
config = {
|
||
'name': f"{genre.title()} {style.title()}",
|
||
'bpm': bpm,
|
||
'key': key,
|
||
'scale': scale,
|
||
'genre': genre,
|
||
'style': style,
|
||
'structure': structure,
|
||
'variant_seed': variant_seed,
|
||
'arrangement_profile': profile['name'],
|
||
'reference_track': reference_resolution.get('reference') if reference_resolution else None,
|
||
'reference_energy_profile': reference_resolution.get('reference_energy_profile') if reference_resolution else None,
|
||
'auto_generate': True,
|
||
'sections': sections,
|
||
'buses': self._build_mix_bus_blueprint(profile, genre, style, reference_resolution),
|
||
'returns': self._build_return_blueprint(profile, genre, style, reference_resolution),
|
||
'master': self._build_master_blueprint(profile, genre, style, reference_resolution),
|
||
'palette': palette or {},
|
||
'tracks': [],
|
||
}
|
||
|
||
# Generar tracks según género
|
||
config['tracks'] = self._generate_tracks_for_genre(genre, style, key, scale, structure, sections, profile)
|
||
config['performance'] = self._build_performance_snapshots(config['tracks'], sections, config.get('returns', []), config.get('buses', []))
|
||
config['mix_automation_summary'] = self._build_mix_automation_summary(config['performance'])
|
||
config['mix_automation_warnings'] = self._verify_automation_safety(config['performance'])
|
||
config['gain_staging_summary'] = self._build_gain_staging_summary(config)
|
||
config['automation'] = self._build_full_automation_blueprint(sections, config.get('buses', []), config.get('returns', []))
|
||
config['transition_events'] = self._generate_transition_events(sections)
|
||
|
||
# Apply density rules to prevent overcrowding
|
||
config['transition_events'] = self._apply_transition_density_rules(config['transition_events'], sections)
|
||
|
||
# Materialize transition events into track blueprints
|
||
config['tracks'] = self._materialize_transition_events(config, config['tracks'])
|
||
|
||
config['locators'] = self._build_locators(sections)
|
||
config['total_bars'] = sum(section['bars'] for section in sections)
|
||
config['total_beats'] = float(config['total_bars'] * 4)
|
||
|
||
# Add section variants summary
|
||
config['section_variants'] = {
|
||
section.get('name', f'section_{i}'): {
|
||
'kind': section.get('kind', 'unknown'),
|
||
'drum_variant': section.get('drum_variant', 'straight'),
|
||
'kick_variant': section.get('kick_variant', (section.get('drum_role_variants') or {}).get('kick', 'straight')),
|
||
'clap_variant': section.get('clap_variant', (section.get('drum_role_variants') or {}).get('clap', 'straight')),
|
||
'hat_closed_variant': section.get('hat_closed_variant', (section.get('drum_role_variants') or {}).get('hat_closed', 'straight')),
|
||
'bass_variant': section.get('bass_variant', 'anchor'),
|
||
'bass_bank_variant': section.get('bass_bank_variant', section.get('bass_variant', 'anchor')),
|
||
'melodic_variant': section.get('melodic_variant', 'motif'),
|
||
'melodic_bank_variant': section.get('melodic_bank_variant', section.get('melodic_variant', 'motif')),
|
||
'transition_fill': section.get('transition_fill', 'none'),
|
||
}
|
||
for i, section in enumerate(sections)
|
||
}
|
||
|
||
# Crear summary
|
||
config['summary'] = f"""
|
||
🎵 Track Generado: {config['name']}
|
||
♩ BPM: {bpm}
|
||
🎹 Key: {key}
|
||
🎨 Style: {style}
|
||
📊 Tracks: {len(config['tracks'])}
|
||
"""
|
||
if config.get('reference_track'):
|
||
config['summary'] += f"🔊 Reference: {config['reference_track'].get('name')}\n"
|
||
|
||
return config
|
||
|
||
def _build_locators(self, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
locators = []
|
||
arrangement_time = 0.0
|
||
for section in sections:
|
||
locators.append({
|
||
'scene_index': int(section.get('index', len(locators))),
|
||
'name': section.get('name', 'SECTION'),
|
||
'bars': int(section.get('bars', 8)),
|
||
'color': int(section.get('color', 10)),
|
||
'time_beats': round(arrangement_time, 3),
|
||
})
|
||
arrangement_time += float(section.get('beats', 0.0) or 0.0)
|
||
return locators
|
||
|
||
def _generate_tracks_for_genre(self, genre: str, style: str, key: str,
|
||
scale: str, structure: str, sections: List[Dict[str, Any]],
|
||
profile: Optional[Dict[str, Any]] = None) -> List[Dict]:
|
||
"""Genera la configuración de tracks según el género"""
|
||
track_specs = []
|
||
style_text = f"{genre} {style}".lower()
|
||
|
||
track_specs.extend([
|
||
('SC TRIGGER', 'sc_trigger', TRACK_COLORS['technical'], 'operator'),
|
||
('KICK', 'kick', TRACK_COLORS['kick'], 'operator'),
|
||
('CLAP', 'clap', TRACK_COLORS['clap'], 'operator'),
|
||
('SNARE FILL', 'snare_fill', TRACK_COLORS['snare'], 'operator'),
|
||
('HAT CLOSED', 'hat_closed', TRACK_COLORS['hat'], 'operator'),
|
||
('HAT OPEN', 'hat_open', TRACK_COLORS['hat'], 'operator'),
|
||
('TOP LOOP', 'top_loop', TRACK_COLORS['hat'], 'operator'),
|
||
('PERCUSSION', 'perc', TRACK_COLORS['perc'], 'operator'),
|
||
('TOM FILL', 'tom_fill', TRACK_COLORS['perc'], 'operator'),
|
||
('SUB BASS', 'sub_bass', TRACK_COLORS['bass'], 'operator'),
|
||
('BASS', 'bass', TRACK_COLORS['bass'], 'operator'),
|
||
('DRONE', 'drone', TRACK_COLORS['pad'], 'analog'),
|
||
('CHORDS', 'chords', TRACK_COLORS['chords'], 'wavetable'),
|
||
('STAB', 'stab', TRACK_COLORS['synth'], 'operator'),
|
||
('PAD', 'pad', TRACK_COLORS['pad'], 'wavetable'),
|
||
('ARP', 'arp', TRACK_COLORS['synth'], 'operator'),
|
||
('LEAD', 'lead', TRACK_COLORS['synth'], 'wavetable'),
|
||
('COUNTER', 'counter', TRACK_COLORS['synth'], 'operator'),
|
||
('CRASH', 'crash', TRACK_COLORS['fx'], 'operator'),
|
||
('REVERSE FX', 'reverse_fx', TRACK_COLORS['fx'], 'analog'),
|
||
('RISER FX', 'riser', TRACK_COLORS['fx'], 'operator'),
|
||
('IMPACT FX', 'impact', TRACK_COLORS['fx'], 'operator'),
|
||
('ATMOS', 'atmos', TRACK_COLORS['fx'], 'analog'),
|
||
])
|
||
tracks = []
|
||
|
||
# Synths/Chords según género
|
||
if genre in ['house', 'trance', 'progressive']:
|
||
tracks.append(self._generate_chord_track(key, scale, genre))
|
||
tracks.append(self._generate_lead_track(key, scale, genre))
|
||
elif genre in ['techno', 'tech-house']:
|
||
if random.random() > 0.3: # 70% de probabilidad
|
||
tracks.append(self._generate_chord_track(key, scale, genre))
|
||
if random.random() > 0.5:
|
||
tracks.append(self._generate_lead_track(key, scale, genre))
|
||
|
||
# FX/Atmósfera para estructuras extended
|
||
if structure in ['extended', 'club'] or random.random() > 0.6:
|
||
tracks.append(self._generate_fx_track())
|
||
|
||
if genre in ['techno', 'tech-house', 'trance']:
|
||
track_specs.insert(8, ('RIDE', 'ride', TRACK_COLORS['ride'], 'operator'))
|
||
if genre in ['house', 'tech-house', 'trance'] or 'latin' in style_text:
|
||
track_specs.insert(14, ('PLUCK', 'pluck', TRACK_COLORS['synth'], 'wavetable'))
|
||
track_specs.insert(15, ('VOCAL CHOP', 'vocal', TRACK_COLORS['vocal'], 'wavetable'))
|
||
elif genre == 'drum-and-bass':
|
||
track_specs = [
|
||
('BREAK', 'kick', TRACK_COLORS['kick'], 'operator'),
|
||
('SNARE', 'clap', TRACK_COLORS['snare'], 'operator'),
|
||
('HATS', 'hat_closed', TRACK_COLORS['hat'], 'operator'),
|
||
('PERCUSSION', 'perc', TRACK_COLORS['perc'], 'operator'),
|
||
('SUB BASS', 'sub_bass', TRACK_COLORS['bass'], 'operator'),
|
||
('REESE', 'bass', TRACK_COLORS['bass'], 'operator'),
|
||
('PAD', 'pad', TRACK_COLORS['pad'], 'wavetable'),
|
||
('ARP', 'arp', TRACK_COLORS['synth'], 'operator'),
|
||
('LEAD', 'lead', TRACK_COLORS['synth'], 'wavetable'),
|
||
('VOCAL', 'vocal', TRACK_COLORS['vocal'], 'wavetable'),
|
||
('RISER FX', 'riser', TRACK_COLORS['fx'], 'operator'),
|
||
('ATMOS', 'atmos', TRACK_COLORS['fx'], 'analog'),
|
||
]
|
||
|
||
blueprint_tracks = []
|
||
active_profile = dict(profile or self._current_generation_profile or {'name': 'default'})
|
||
for name, role, color, device in track_specs:
|
||
clips = self._build_scene_clips(role, genre, style, key, scale, sections)
|
||
if not clips:
|
||
continue
|
||
|
||
mix_profile = dict(ROLE_MIX.get(role, {}))
|
||
mix_profile['sends'] = self._extend_parallel_sends(role, mix_profile.get('sends', {}))
|
||
mix_profile = self._shape_mix_profile(role, mix_profile, active_profile, style)
|
||
track = {
|
||
'name': name,
|
||
'type': 'midi',
|
||
'role': role,
|
||
'bus': self._resolve_bus_for_role(role),
|
||
'device': device,
|
||
'color': color,
|
||
'volume': mix_profile.get('volume', 0.72),
|
||
'pan': mix_profile.get('pan', 0.0),
|
||
'sends': dict(mix_profile.get('sends', {})),
|
||
'fx_chain': self._shape_role_fx_chain(role, active_profile, style),
|
||
'clips': clips,
|
||
}
|
||
track['clip'] = dict(clips[0])
|
||
|
||
# Agregar metadata de variación al blueprint
|
||
if role in SECTION_VARIATION_CONFIG:
|
||
track['section_variation'] = SECTION_VARIATION_CONFIG[role]
|
||
track['can_vary_by_section'] = True
|
||
|
||
blueprint_tracks.append(track)
|
||
|
||
return blueprint_tracks
|
||
|
||
def _build_sections(self, structure: str, style: str = "", variant_seed: Optional[int] = None,
|
||
profile: Optional[Dict[str, Any]] = None) -> List[Dict[str, Any]]:
|
||
structure_key = structure.lower()
|
||
rng = random.Random(variant_seed) if variant_seed is not None else random
|
||
blueprint_options = SECTION_BLUEPRINT_VARIANTS.get(structure_key)
|
||
if blueprint_options:
|
||
if 'latin' in style and structure_key == 'club' and len(blueprint_options) > 1:
|
||
blueprint = rng.choice(blueprint_options[1:])
|
||
else:
|
||
blueprint = rng.choice(blueprint_options)
|
||
else:
|
||
blueprint = SECTION_BLUEPRINTS.get(structure_key, SECTION_BLUEPRINTS['standard'])
|
||
sections = []
|
||
style_text = style.lower() if style else ""
|
||
profile_name = str((profile or {}).get('name', 'default')).lower()
|
||
for index, (name, bars, color, kind, energy) in enumerate(blueprint):
|
||
if kind == 'intro':
|
||
drum_variants = ['straight', 'skip']
|
||
bass_variants = ['anchor', 'pedal']
|
||
melodic_variants = ['motif', 'response']
|
||
elif kind == 'build':
|
||
drum_variants = ['shuffle', 'pressure', 'straight']
|
||
bass_variants = ['bounce', 'syncopated']
|
||
melodic_variants = ['lift', 'response']
|
||
elif kind == 'break':
|
||
drum_variants = ['skip', 'shuffle']
|
||
bass_variants = ['pedal', 'anchor']
|
||
melodic_variants = ['drone', 'response']
|
||
elif kind == 'outro':
|
||
drum_variants = ['straight', 'skip']
|
||
bass_variants = ['anchor', 'pedal']
|
||
melodic_variants = ['motif', 'descend']
|
||
else:
|
||
drum_variants = ['straight', 'pressure', 'shuffle']
|
||
bass_variants = ['syncopated', 'bounce', 'anchor']
|
||
melodic_variants = ['lift', 'motif', 'descend']
|
||
|
||
swing_pool = [0.0, 0.015, 0.025]
|
||
if 'latin' in style_text or profile_name in ['jackin', 'swing']:
|
||
swing_pool.extend([0.035, 0.045, 0.055])
|
||
|
||
pan_variant = rng.choice(['narrow', 'wide', 'tilt_left', 'tilt_right'])
|
||
if kind in ['intro', 'outro'] and rng.random() > 0.5:
|
||
pan_variant = 'narrow'
|
||
if kind == 'break' and rng.random() > 0.4:
|
||
pan_variant = 'wide'
|
||
|
||
section_data = {
|
||
'index': index,
|
||
'name': name,
|
||
'bars': int(bars),
|
||
'beats': float(bars * 4),
|
||
'color': color,
|
||
'kind': kind,
|
||
'energy': int(energy),
|
||
'density': round(min(1.35, max(0.68, 0.78 + (energy * 0.08) + rng.uniform(-0.08, 0.14))), 3),
|
||
'swing': round(rng.choice(swing_pool), 3),
|
||
'tension': int(min(5, max(1, energy + rng.choice([-1, 0, 0, 1])))),
|
||
'drum_variant': rng.choice(drum_variants),
|
||
'bass_variant': rng.choice(bass_variants),
|
||
'melodic_variant': rng.choice(melodic_variants),
|
||
'pan_variant': pan_variant,
|
||
'transition_fill': rng.choice(['none', 'snare', 'tom', 'reverse', 'impact']),
|
||
}
|
||
sections.append(self._ensure_section_pattern_variants(section_data))
|
||
# Check for excessive repetition and force variation if needed
|
||
sections = self._check_section_repetition(sections)
|
||
return sections
|
||
|
||
def _role_intensity(self, role: str, section: Dict[str, Any]) -> int:
|
||
kind = section.get('kind', 'drop')
|
||
energy = int(section.get('energy', 1))
|
||
role_energy = ROLE_ACTIVITY.get(role, {}).get(kind, 0)
|
||
return min(max(role_energy, 0), max(1, energy + 1))
|
||
|
||
def _build_scene_clips(self, role: str, genre: str, style: str, key: str,
|
||
scale: str, sections: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
||
clips = []
|
||
for section in sections:
|
||
notes = self._render_scene_notes(role, genre, style, key, scale, section)
|
||
if not notes:
|
||
continue
|
||
|
||
clips.append({
|
||
'scene_index': section['index'],
|
||
'length': section['beats'],
|
||
'name': f"{role.upper()} - {section['name']}",
|
||
'notes': notes,
|
||
})
|
||
return clips
|
||
|
||
def _render_scene_notes(self, role: str, genre: str, style: str, key: str,
|
||
scale: str, section: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
intensity = self._role_intensity(role, section)
|
||
if intensity <= 0:
|
||
return []
|
||
|
||
if role in ['sc_trigger', 'kick', 'clap', 'snare_fill', 'hat_closed', 'hat_open', 'top_loop', 'perc', 'tom_fill', 'ride', 'crash']:
|
||
return self._render_drum_scene(role, genre, style, section, intensity)
|
||
if role in ['sub_bass', 'bass']:
|
||
return self._render_bass_scene(role, genre, style, key, section)
|
||
if role in ['chords', 'stab', 'pad', 'pluck', 'arp', 'lead', 'counter']:
|
||
return self._render_musical_scene(role, genre, key, scale, section)
|
||
if role in ['drone', 'reverse_fx', 'riser', 'impact', 'atmos', 'vocal']:
|
||
return self._render_fx_scene(role, key, section)
|
||
return []
|
||
|
||
def _render_drum_scene(self, role: str, genre: str, style: str,
|
||
section: Dict[str, Any], intensity: int) -> List[Dict[str, Any]]:
|
||
total_length = float(section['beats'])
|
||
kind = section['kind']
|
||
style_text = f"{genre} {style}".lower()
|
||
|
||
if role == 'sc_trigger':
|
||
pattern = [self._make_note(24, beat, 0.12, 127) for beat in [0.0, 1.0, 2.0, 3.0]]
|
||
if kind == 'break':
|
||
pattern = [self._make_note(24, beat, 0.1, 118) for beat in [0.0, 2.0]]
|
||
return self._repeat_pattern(pattern, total_length, 4.0)
|
||
|
||
if role == 'kick':
|
||
if genre == 'drum-and-bass':
|
||
pattern = [
|
||
self._make_note(36, 0.0, 0.25, 122),
|
||
self._make_note(36, 0.75, 0.2, 104),
|
||
self._make_note(36, 1.5, 0.2, 112),
|
||
self._make_note(36, 2.0, 0.25, 124),
|
||
self._make_note(36, 2.75, 0.2, 100),
|
||
self._make_note(36, 3.25, 0.2, 92),
|
||
]
|
||
elif kind == 'break':
|
||
pattern = [
|
||
self._make_note(36, 0.0, 0.25, 118),
|
||
self._make_note(36, 2.0, 0.25, 110),
|
||
]
|
||
else:
|
||
pattern = [self._make_note(36, beat, 0.25, 126 if beat == 0 else 118) for beat in [0.0, 1.0, 2.0, 3.0]]
|
||
if intensity >= 4 and genre in ['techno', 'tech-house']:
|
||
pattern.append(self._make_note(36, 3.5, 0.15, 94))
|
||
notes = self._repeat_pattern(pattern, total_length, 4.0)
|
||
if kind in ['build', 'drop', 'outro']:
|
||
notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'clap':
|
||
pitch = 38 if genre == 'drum-and-bass' else 39
|
||
if kind == 'intro':
|
||
pattern = [self._make_note(pitch, 3.0, 0.2, 88)]
|
||
elif kind == 'break':
|
||
pattern = [self._make_note(pitch, 1.0, 0.2, 84)]
|
||
else:
|
||
pattern = [
|
||
self._make_note(pitch, 1.0, 0.25, 108),
|
||
self._make_note(pitch, 3.0, 0.25, 108),
|
||
]
|
||
notes = self._repeat_pattern(pattern, total_length, 4.0)
|
||
if kind in ['build', 'drop']:
|
||
notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'snare_fill':
|
||
if kind not in ['build', 'break', 'drop']:
|
||
return []
|
||
if str(section.get('transition_fill', 'snare')).lower() not in ['snare', 'impact'] and kind != 'drop':
|
||
return []
|
||
fill_span = 2.0 if kind == 'build' and total_length >= 8.0 else 1.0
|
||
fill_start = max(0.0, total_length - fill_span)
|
||
step = 0.25 if intensity <= 2 else 0.125
|
||
velocity = 76
|
||
notes = []
|
||
current = fill_start
|
||
while current < total_length - 0.01:
|
||
notes.append(self._make_note(38, current, 0.08 if step < 0.2 else 0.12, min(124, velocity)))
|
||
current += step
|
||
velocity += 3
|
||
if kind == 'drop':
|
||
notes.insert(0, self._make_note(38, 0.0, 0.15, 102))
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'hat_closed':
|
||
if intensity <= 1:
|
||
pattern = [self._make_note(42, beat, 0.1, 86) for beat in [0.5, 1.5, 2.5, 3.5]]
|
||
elif intensity == 2:
|
||
pattern = [self._make_note(42, step * 0.5, 0.1, 90 if step % 2 == 0 else 72) for step in range(8)]
|
||
else:
|
||
pattern = [self._make_note(42, step * 0.5, 0.1, 92 if step % 2 == 0 else 74) for step in range(8)]
|
||
pattern.extend([self._make_note(42, 1.75, 0.08, 64), self._make_note(42, 3.75, 0.08, 62)])
|
||
notes = self._repeat_pattern(pattern, total_length, 4.0)
|
||
if kind in ['build', 'drop', 'outro']:
|
||
notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'hat_open':
|
||
if kind in ['intro', 'break'] and intensity <= 1:
|
||
return []
|
||
pattern = [self._make_note(46, 3.5, 0.35, 82)]
|
||
if intensity >= 3:
|
||
pattern.append(self._make_note(46, 1.5, 0.25, 74))
|
||
notes = self._repeat_pattern(pattern, total_length, 4.0)
|
||
if kind in ['build', 'drop']:
|
||
notes = self._merge_section_notes(notes, self._build_drum_fill(role, total_length, intensity), total_length)
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'top_loop':
|
||
if kind in ['intro', 'break'] and intensity <= 1:
|
||
return []
|
||
pattern = [
|
||
self._make_note(44, 0.25, 0.08, 56),
|
||
self._make_note(44, 0.75, 0.08, 62),
|
||
self._make_note(44, 1.25, 0.08, 58),
|
||
self._make_note(44, 1.75, 0.08, 66),
|
||
self._make_note(44, 2.25, 0.08, 58),
|
||
self._make_note(44, 2.75, 0.08, 64),
|
||
self._make_note(44, 3.25, 0.08, 60),
|
||
self._make_note(44, 3.75, 0.08, 68),
|
||
]
|
||
if 'latin' in style_text:
|
||
pattern.extend([
|
||
self._make_note(54, 0.5, 0.08, 52),
|
||
self._make_note(54, 2.5, 0.08, 54),
|
||
])
|
||
if intensity >= 3:
|
||
pattern.extend([
|
||
self._make_note(44, 1.125, 0.06, 48),
|
||
self._make_note(44, 3.125, 0.06, 50),
|
||
])
|
||
return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)
|
||
|
||
if role == 'perc':
|
||
if kind in ['intro', 'outro'] and intensity <= 1:
|
||
return []
|
||
pattern = [
|
||
self._make_note(37, 0.75, 0.1, 62),
|
||
self._make_note(37, 1.25, 0.1, 58),
|
||
self._make_note(37, 2.75, 0.1, 64),
|
||
self._make_note(50, 3.25, 0.12, 70),
|
||
]
|
||
if 'latin' in style_text:
|
||
pattern.extend([
|
||
self._make_note(64, 1.75, 0.12, 68),
|
||
self._make_note(64, 2.125, 0.12, 64),
|
||
])
|
||
if intensity >= 3:
|
||
pattern.extend([self._make_note(37, 0.25, 0.1, 56), self._make_note(47, 2.25, 0.1, 68)])
|
||
return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)
|
||
|
||
if role == 'tom_fill':
|
||
if kind not in ['build', 'drop']:
|
||
return []
|
||
if str(section.get('transition_fill', 'tom')).lower() not in ['tom', 'impact'] and kind != 'drop':
|
||
return []
|
||
fill_start = max(0.0, total_length - 1.0)
|
||
sequence = [47, 50, 45, 47, 50]
|
||
velocities = [72, 76, 80, 88, 96]
|
||
notes = []
|
||
for index, pitch in enumerate(sequence):
|
||
start = fill_start + (index * 0.2)
|
||
if start >= total_length:
|
||
break
|
||
notes.append(self._make_note(pitch, start, 0.18, velocities[index]))
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
if role == 'ride':
|
||
if kind not in ['build', 'drop', 'outro']:
|
||
return []
|
||
pattern = [self._make_note(51, float(beat), 0.2, 82) for beat in range(4)]
|
||
if intensity >= 3:
|
||
pattern.extend([self._make_note(51, beat + 0.5, 0.15, 64) for beat in range(4)])
|
||
return self._vary_drum_notes(self._repeat_pattern(pattern, total_length, 4.0), role, section, total_length)
|
||
|
||
if role == 'crash':
|
||
if kind not in ['build', 'drop', 'break', 'outro']:
|
||
return []
|
||
hit_positions = [0.0]
|
||
if kind == 'drop' and total_length >= 16.0:
|
||
hit_positions.append(8.0)
|
||
if kind == 'outro' and total_length >= 8.0:
|
||
hit_positions.append(total_length - 4.0)
|
||
notes = [
|
||
self._make_note(49, position, min(1.5, max(0.25, total_length - position)), 82 if position == 0.0 else 70)
|
||
for position in hit_positions
|
||
if position < total_length
|
||
]
|
||
return self._vary_drum_notes(notes, role, section, total_length)
|
||
|
||
return []
|
||
|
||
def _bass_style_for_section(self, genre: str, style: str, role: str, section_kind: str) -> str:
|
||
style_text = f"{genre} {style}".lower()
|
||
if role == 'sub_bass':
|
||
return 'minimal' if section_kind != 'drop' else 'offbeat'
|
||
if 'acid' in style_text:
|
||
return 'acid'
|
||
if genre == 'house':
|
||
return 'offbeat'
|
||
if genre == 'drum-and-bass':
|
||
return 'rolling'
|
||
if section_kind in ['intro', 'outro', 'break']:
|
||
return 'minimal'
|
||
if genre == 'tech-house':
|
||
return 'offbeat'
|
||
return 'rolling'
|
||
|
||
def _render_bass_scene(self, role: str, genre: str, style: str, key: str,
|
||
section: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
total_length = float(section['beats'])
|
||
kind = section['kind']
|
||
scale_name = 'minor' if 'm' in key.lower() else 'major'
|
||
|
||
if kind == 'break':
|
||
notes = self._build_pad_motion(key, scale_name, total_length, 2, 4.0)
|
||
else:
|
||
notes = self.create_bassline(key, self._bass_style_for_section(genre, style, role, kind), total_length)
|
||
|
||
if role == 'sub_bass':
|
||
notes = self._transpose_notes(notes, -12)
|
||
notes = self._scale_note_lengths(notes, 1.35, minimum=0.2)
|
||
notes = self._vary_bass_notes(notes, role, key, section, total_length)
|
||
if kind in ['build', 'drop'] and total_length >= 8.0:
|
||
turnaround = self._build_turnaround_notes(key, scale_name, total_length, 2 if role == 'bass' else 1, 88 if role == 'bass' else 80)
|
||
notes = self._merge_section_notes(notes, turnaround, total_length)
|
||
return notes
|
||
|
||
def _render_musical_scene(self, role: str, genre: str, key: str, scale: str,
|
||
section: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
total_length = float(section['beats'])
|
||
kind = section['kind']
|
||
|
||
if role == 'pad':
|
||
notes = self._build_pad_motion(key, scale, total_length, 4, 8.0 if kind == 'break' else 4.0)
|
||
return self._vary_melodic_notes(notes, role, key, scale, section, total_length)
|
||
|
||
if role == 'chords':
|
||
progression_type = 'techno' if genre in ['techno', 'tech-house'] else ('trance' if genre == 'trance' else 'house')
|
||
notes = self.create_chord_progression(key, progression_type, total_length)
|
||
notes = self._scale_note_lengths(notes, 1.15, minimum=0.25)
|
||
return self._vary_melodic_notes(notes, role, key, scale, section, total_length)
|
||
|
||
if role == 'stab':
|
||
notes = self.create_chord_progression(key, 'techno' if genre in ['techno', 'tech-house'] else 'house', total_length)
|
||
notes = self._scale_note_lengths(notes, 0.4, minimum=0.1)
|
||
shifted = []
|
||
for note in notes:
|
||
start = float(note['start']) + (0.5 if int(float(note['start'])) % 2 == 0 else 0.0)
|
||
shifted.append(self._make_note(note['pitch'], start, note['duration'], min(118, note['velocity'] + 6)))
|
||
return self._vary_melodic_notes(shifted, role, key, scale, section, total_length)
|
||
|
||
if role == 'pluck':
|
||
notes = self.create_melody(key, scale, total_length, genre)
|
||
notes = self._scale_note_lengths(notes, 0.55, minimum=0.12)
|
||
return self._vary_melodic_notes(notes, role, key, scale, section, total_length)
|
||
|
||
notes = self.create_melody(key, scale, total_length, genre)
|
||
if role == 'arp':
|
||
notes = self._scale_note_lengths(notes, 0.45, minimum=0.1)
|
||
elif role == 'lead':
|
||
notes = self._transpose_notes(notes, 12)
|
||
elif role == 'counter':
|
||
sparse = []
|
||
for note in notes:
|
||
start = float(note['start'])
|
||
if (start % 4.0) < 2.0:
|
||
continue
|
||
sparse.append(self._make_note(note['pitch'] - 12, start, max(0.2, float(note['duration']) * 0.8), max(50, int(note['velocity']) - 10)))
|
||
notes = sparse
|
||
notes = self._vary_melodic_notes(notes, role, key, scale, section, total_length)
|
||
if role in ['lead', 'arp', 'pluck', 'counter'] and kind in ['build', 'drop'] and total_length >= 8.0:
|
||
notes = self._merge_section_notes(notes, self._build_turnaround_notes(key, scale, total_length, 5, 84), total_length)
|
||
return notes
|
||
|
||
def _render_fx_scene(self, role: str, key: str, section: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||
total_length = float(section['beats'])
|
||
kind = section.get('kind', 'drop')
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, 5)
|
||
rng = self._section_rng(section, role, salt=19)
|
||
|
||
if role == 'drone':
|
||
notes = [
|
||
self._make_note(root_midi - 12, 0.0, min(total_length, 8.0 if kind == 'break' else total_length), 58),
|
||
self._make_note(root_midi - 5, max(0.0, total_length / 2.0), min(total_length / 2.0, 8.0), 52),
|
||
]
|
||
if kind in ['build', 'drop'] and total_length >= 12.0:
|
||
notes.append(self._make_note(root_midi + 2, max(0.0, total_length - 6.0), 4.0, 48))
|
||
return notes
|
||
|
||
if role == 'reverse_fx':
|
||
if str(section.get('transition_fill', 'reverse')).lower() not in ['reverse', 'impact'] and kind not in ['break', 'build']:
|
||
return []
|
||
notes = []
|
||
for span, offset, velocity in ((4.0, 4.0, 70), (2.0, 2.0, 64), (1.0, 1.0, 58)):
|
||
if total_length >= offset:
|
||
start = max(0.0, total_length - offset)
|
||
notes.append(self._make_note(root_midi + 12, start, min(span, total_length - start), velocity))
|
||
if kind == 'build' and total_length >= 16.0 and rng.random() > 0.35:
|
||
notes.append(self._make_note(root_midi + 7, max(0.0, total_length - 8.0), 1.5, 56))
|
||
return notes
|
||
|
||
if role == 'riser':
|
||
notes = []
|
||
sweep_start = max(0.0, total_length - min(8.0, total_length))
|
||
for offset, pitch, velocity in ((0.0, root_midi + 7, 64), (2.0, root_midi + 12, 70), (4.0, root_midi + 19, 74), (6.0, root_midi + 24, 78)):
|
||
start = sweep_start + offset
|
||
if start < total_length:
|
||
notes.append(self._make_note(pitch, start, min(2.0, total_length - start), velocity))
|
||
if kind == 'build' and total_length >= 8.0:
|
||
notes.extend([
|
||
self._make_note(root_midi + 12, max(0.0, total_length - 2.0), 0.5, 82),
|
||
self._make_note(root_midi + 19, max(0.0, total_length - 1.0), 0.45, 86),
|
||
])
|
||
return notes
|
||
|
||
if role == 'impact':
|
||
if kind in ['intro', 'outro'] and str(section.get('transition_fill', 'impact')).lower() != 'impact':
|
||
return []
|
||
notes = [self._make_note(root_midi + 7, 0.0, 0.5, 82)]
|
||
if total_length >= 8.0 and kind in ['build', 'drop']:
|
||
notes.append(self._make_note(root_midi + 12, total_length - 0.5, 0.45, 76))
|
||
if kind == 'drop' and total_length >= 16.0 and rng.random() > 0.4:
|
||
notes.append(self._make_note(root_midi + 10, 8.0, 0.35, 72))
|
||
return notes
|
||
|
||
if role == 'atmos':
|
||
notes = [
|
||
self._make_note(root_midi, 0.0, min(8.0, total_length), 54),
|
||
self._make_note(root_midi + 7, max(0.0, total_length / 2.0), min(8.0, total_length / 2.0), 50),
|
||
]
|
||
if kind in ['intro', 'break', 'outro'] and total_length >= 12.0:
|
||
notes.append(self._make_note(root_midi + 12, max(0.0, total_length - 4.0), min(4.0, total_length), 46))
|
||
return notes
|
||
|
||
if role == 'vocal':
|
||
notes = []
|
||
if kind == 'intro':
|
||
base_positions = [7.5, 15.5]
|
||
elif kind == 'build':
|
||
base_positions = [1.5, 3.5, 5.5, 7.5]
|
||
if total_length >= 16.0:
|
||
base_positions.extend([11.5, 13.5, 15.5])
|
||
elif kind == 'drop':
|
||
base_positions = [1.5, 2.75, 5.5, 6.75]
|
||
if total_length >= 16.0:
|
||
base_positions.extend([9.5, 10.75, 13.5, 14.75])
|
||
elif kind == 'break':
|
||
base_positions = [3.5, 11.5]
|
||
else:
|
||
base_positions = [1.5, 5.5]
|
||
|
||
for index, pos in enumerate(base_positions):
|
||
if pos >= total_length:
|
||
continue
|
||
pitch = root_midi + (10 if kind == 'drop' and index % 2 else 3)
|
||
duration = 0.22 if kind == 'drop' else 0.3
|
||
velocity = 80 if kind in ['build', 'drop'] else 72
|
||
if rng.random() > 0.82:
|
||
pitch += 12
|
||
notes.append(self._make_note(pitch, pos, duration, velocity))
|
||
|
||
if kind == 'build' and total_length >= 8.0:
|
||
notes.append(self._make_note(root_midi + 15, max(0.0, total_length - 0.75), 0.22, 84))
|
||
return notes
|
||
|
||
return []
|
||
|
||
def _build_pad_motion(self, key: str, scale_name: str, total_length: float,
|
||
octave: int = 4, sustain_beats: float = 4.0) -> List[Dict[str, Any]]:
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, octave)
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
progression = random.choice(CHORD_PROGRESSIONS.get('techno' if 'm' in key.lower() else 'house', CHORD_PROGRESSIONS['techno']))
|
||
notes = []
|
||
bars = max(1, int(total_length / 4.0))
|
||
|
||
for bar in range(bars):
|
||
degree = progression[bar % len(progression)] - 1
|
||
chord_root = scale_notes[degree % len(scale_notes)]
|
||
start = float(bar * 4.0)
|
||
duration = min(sustain_beats, total_length - start)
|
||
for interval in [0, 7, 12]:
|
||
notes.append(self._make_note(chord_root + interval, start, duration, 66))
|
||
return notes
|
||
|
||
def _generate_drum_tracks(self, genre: str, style: str) -> List[Dict]:
|
||
"""Genera tracks de baterÃa"""
|
||
tracks = []
|
||
|
||
# Kick siempre
|
||
tracks.append({
|
||
'name': 'Kick',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['kick'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 4.0,
|
||
'notes': self._create_kick_pattern(genre, style)
|
||
}
|
||
})
|
||
|
||
# Snare/Clap
|
||
tracks.append({
|
||
'name': 'Clap',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['clap'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 4.0,
|
||
'notes': self._create_clap_pattern(genre, style)
|
||
}
|
||
})
|
||
|
||
# Hi-hats
|
||
tracks.append({
|
||
'name': 'HiHat',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['hat'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 4.0,
|
||
'notes': self._create_hat_pattern(genre, style)
|
||
}
|
||
})
|
||
|
||
# Percusión extra para estilos más complejos
|
||
if style in ['latin', 'afro', 'groovy', 'complex']:
|
||
tracks.append({
|
||
'name': 'Percussion',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['hat'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 4.0,
|
||
'notes': self._create_perc_pattern(genre, style)
|
||
}
|
||
})
|
||
|
||
return tracks
|
||
|
||
def _generate_bass_track(self, key: str, scale: str, genre: str, style: str) -> Dict:
|
||
"""Genera un track de bajo"""
|
||
notes = self.create_bassline(key, style, 16.0)
|
||
|
||
return {
|
||
'name': 'Bass',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['bass'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 16.0,
|
||
'notes': notes
|
||
}
|
||
}
|
||
|
||
def _generate_chord_track(self, key: str, scale: str, genre: str) -> Dict:
|
||
"""Genera un track de acordes"""
|
||
notes = self.create_chord_progression(key, genre, 16.0)
|
||
|
||
return {
|
||
'name': 'Chords',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['chords'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 16.0,
|
||
'notes': notes
|
||
}
|
||
}
|
||
|
||
def _generate_lead_track(self, key: str, scale: str, genre: str) -> Dict:
|
||
"""Genera un track lead/melódico"""
|
||
notes = self.create_melody(key, scale, 16.0, genre)
|
||
|
||
return {
|
||
'name': 'Lead',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['synth'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 16.0,
|
||
'notes': notes
|
||
}
|
||
}
|
||
|
||
def _generate_fx_track(self) -> Dict:
|
||
"""Genera un track de FX/Atmósfera"""
|
||
return {
|
||
'name': 'FX',
|
||
'type': 'midi',
|
||
'color': TRACK_COLORS['fx'],
|
||
'clip': {
|
||
'slot': 0,
|
||
'length': 16.0,
|
||
'notes': self._create_fx_notes()
|
||
}
|
||
}
|
||
|
||
# =========================================================================
|
||
# PATRONES DE BATERÃA
|
||
# =========================================================================
|
||
|
||
def _create_kick_pattern(self, genre: str, style: str) -> List[Dict]:
|
||
"""Crea patrón de kick"""
|
||
notes = []
|
||
|
||
if style == 'minimal':
|
||
# Kick en 1 y 2.5
|
||
for bar in range(4):
|
||
notes.append({'pitch': 36, 'start': bar * 4.0, 'duration': 0.25, 'velocity': 120})
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + 2.5, 'duration': 0.25, 'velocity': 110})
|
||
elif style == 'four-on-the-floor' or genre in ['house', 'tech-house']:
|
||
# 4/4 clásico
|
||
for bar in range(4):
|
||
for beat in range(4):
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': 127})
|
||
else: # Default techno
|
||
for bar in range(4):
|
||
for beat in range(4):
|
||
vel = 127 if beat == 0 else 115
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': vel})
|
||
|
||
return notes
|
||
|
||
def _create_clap_pattern(self, genre: str, style: str) -> List[Dict]:
|
||
"""Crea patrón de clap/snare"""
|
||
notes = []
|
||
|
||
# Claps en 2 y 4 (beats 1 y 3 en 0-indexed)
|
||
for bar in range(4):
|
||
notes.append({'pitch': 40, 'start': bar * 4.0 + 1.0, 'duration': 0.25, 'velocity': 110})
|
||
notes.append({'pitch': 40, 'start': bar * 4.0 + 3.0, 'duration': 0.25, 'velocity': 110})
|
||
|
||
# Snare adicional para DnB/Jungle
|
||
if genre == 'drum-and-bass':
|
||
for bar in range(4):
|
||
notes.append({'pitch': 38, 'start': bar * 4.0 + 1.75, 'duration': 0.1, 'velocity': 90})
|
||
notes.append({'pitch': 38, 'start': bar * 4.0 + 2.25, 'duration': 0.1, 'velocity': 85})
|
||
|
||
return notes
|
||
|
||
def _create_hat_pattern(self, genre: str, style: str) -> List[Dict]:
|
||
"""Crea patrón de hi-hats"""
|
||
notes = []
|
||
|
||
if style in ['minimal', 'dub']:
|
||
# Off-bats simples
|
||
for bar in range(4):
|
||
for beat in range(4):
|
||
notes.append({'pitch': 42, 'start': bar * 4.0 + beat + 0.5, 'duration': 0.1, 'velocity': 90})
|
||
else:
|
||
# 8vos con variación
|
||
for bar in range(4):
|
||
for beat in range(4):
|
||
for sub in range(2):
|
||
time = bar * 4.0 + beat + sub * 0.5
|
||
vel = 90 if sub == 0 else 70
|
||
notes.append({'pitch': 42, 'start': time, 'duration': 0.1, 'velocity': vel})
|
||
|
||
# Open hats ocasionales
|
||
if style not in ['minimal']:
|
||
for bar in range(4):
|
||
notes.append({'pitch': 46, 'start': bar * 4.0 + 3.5, 'duration': 0.5, 'velocity': 80})
|
||
|
||
return notes
|
||
|
||
def _create_perc_pattern(self, genre: str, style: str) -> List[Dict]:
|
||
"""Crea patrón de percusión extra"""
|
||
notes = []
|
||
|
||
for bar in range(4):
|
||
# Shakers/congas en 16vos
|
||
for i in range(16):
|
||
time = bar * 4.0 + i * 0.25
|
||
if i % 4 != 0: # Skip downbeats
|
||
vel = 60 + random.randint(-10, 10)
|
||
notes.append({'pitch': 37, 'start': time, 'duration': 0.1, 'velocity': vel})
|
||
|
||
return notes
|
||
|
||
def _create_fx_notes(self) -> List[Dict]:
|
||
"""Crea notas para FX/atmósfera"""
|
||
notes = []
|
||
|
||
# Swells y risers
|
||
for bar in [0, 2]:
|
||
# Nota larga ascendente
|
||
notes.append({'pitch': 84, 'start': bar * 4.0 + 3.0, 'duration': 1.0, 'velocity': 70})
|
||
|
||
return notes
|
||
|
||
# =========================================================================
|
||
# CREACIÓN DE PATRONES PARA MCP
|
||
# =========================================================================
|
||
|
||
def create_drum_pattern(self, style: str, pattern_type: str, length: float) -> List[Dict]:
|
||
"""Crea un patrón de baterÃa completo para usar con MCP"""
|
||
notes = []
|
||
bars = int(length / 4.0)
|
||
|
||
if pattern_type == 'kick-only':
|
||
for bar in range(bars):
|
||
for beat in range(4):
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + beat, 'duration': 0.25, 'velocity': 127})
|
||
|
||
elif pattern_type == 'hats-only':
|
||
for bar in range(bars):
|
||
for beat in range(4):
|
||
notes.append({'pitch': 42, 'start': bar * 4.0 + beat + 0.5, 'duration': 0.1, 'velocity': 90})
|
||
|
||
elif pattern_type == 'minimal':
|
||
for bar in range(bars):
|
||
notes.append({'pitch': 36, 'start': bar * 4.0, 'duration': 0.25, 'velocity': 127})
|
||
notes.append({'pitch': 40, 'start': bar * 4.0 + 2.0, 'duration': 0.25, 'velocity': 110})
|
||
notes.append({'pitch': 42, 'start': bar * 4.0 + 2.5, 'duration': 0.1, 'velocity': 80})
|
||
|
||
elif pattern_type == 'dembow':
|
||
# Patron dembow caracteristico del reggaeton
|
||
# K . . . S . K . | K . . . S . . .
|
||
# 1 e & a 2 e & a | 3 e & a 4 e & a
|
||
for bar in range(bars):
|
||
# Kick en 1, 1.75 (el "y" del 2), 3
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + 0.0, 'duration': 0.25, 'velocity': 127}) # Beat 1
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + 1.75, 'duration': 0.25, 'velocity': 115}) # "Ghost" kick
|
||
notes.append({'pitch': 36, 'start': bar * 4.0 + 3.0, 'duration': 0.25, 'velocity': 127}) # Beat 3
|
||
# Snare/Clap en 2 y 4
|
||
notes.append({'pitch': 40, 'start': bar * 4.0 + 1.0, 'duration': 0.25, 'velocity': 110}) # Beat 2
|
||
notes.append({'pitch': 40, 'start': bar * 4.0 + 3.75, 'duration': 0.25, 'velocity': 100}) # Anticipo beat 4
|
||
# Hi-hats cada 1/8 con swing
|
||
for eighth in range(8):
|
||
time = bar * 4.0 + eighth * 0.5
|
||
# Acentos en 1, 2, 3, 4
|
||
vel = 100 if eighth % 2 == 0 else 80
|
||
notes.append({'pitch': 42, 'start': time, 'duration': 0.1, 'velocity': vel})
|
||
|
||
else: # full
|
||
notes.extend(self._create_kick_pattern(style, 'standard'))
|
||
notes.extend(self._create_clap_pattern(style, 'standard'))
|
||
notes.extend(self._create_hat_pattern(style, 'standard'))
|
||
|
||
return notes
|
||
|
||
def create_bassline(self, key: str, style: str, length: float) -> List[Dict]:
|
||
"""Crea una lÃnea de bajo musical"""
|
||
notes = []
|
||
|
||
# Parsear key
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
is_minor = 'm' in key.lower()
|
||
scale_name = 'minor' if is_minor else 'major'
|
||
|
||
root_midi = self.note_name_to_midi(root_note, 2) # Octava 2 para bajo
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
|
||
bars = int(length / 4.0)
|
||
|
||
if style == 'rolling':
|
||
# Bass en 16vos
|
||
for bar in range(bars):
|
||
for beat in range(4):
|
||
for sub in range(4):
|
||
time = bar * 4.0 + beat + sub * 0.25
|
||
if sub == 0:
|
||
pitch = root_midi
|
||
vel = 120
|
||
elif sub == 2:
|
||
pitch = scale_notes[4] if len(scale_notes) > 4 else root_midi + 7
|
||
vel = 100
|
||
else:
|
||
pitch = root_midi
|
||
vel = 80 if sub % 2 == 0 else 70
|
||
|
||
notes.append({'pitch': pitch, 'start': time, 'duration': 0.2, 'velocity': vel})
|
||
|
||
elif style == 'minimal':
|
||
# Solo en beats 1 y 3
|
||
for bar in range(bars):
|
||
for beat in [0, 2]:
|
||
time = bar * 4.0 + beat
|
||
notes.append({'pitch': root_midi, 'start': time, 'duration': 1.5, 'velocity': 110})
|
||
|
||
elif style == 'offbeat':
|
||
# Notas en off-beats (house tÃpico)
|
||
for bar in range(bars):
|
||
for beat in range(4):
|
||
time = bar * 4.0 + beat + 0.5
|
||
pitch = root_midi if beat % 2 == 0 else scale_notes[3]
|
||
notes.append({'pitch': pitch, 'start': time, 'duration': 0.4, 'velocity': 100})
|
||
|
||
elif style == 'acid':
|
||
# Estilo TB-303 con slides
|
||
for bar in range(bars):
|
||
for i in range(8):
|
||
time = bar * 4.0 + i * 0.5
|
||
pitch = root_midi + random.choice([0, 3, 5, 7, 10])
|
||
vel = 90 + random.randint(-20, 20)
|
||
notes.append({'pitch': pitch, 'start': time, 'duration': 0.4, 'velocity': min(127, max(60, vel))})
|
||
|
||
elif style == 'bouncy':
|
||
# Estilo bouncy para reggaeton - notas cortas con "bounce"
|
||
for bar in range(bars):
|
||
# Pattern: bump en 1, silencio, nota de apoyo en el "3"
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 0.0, 'duration': 0.3, 'velocity': 120}) # Bump fuerte
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 1.75, 'duration': 0.15, 'velocity': 90}) # Ghost note
|
||
notes.append({'pitch': scale_notes[4] if len(scale_notes) > 4 else root_midi + 7,
|
||
'start': bar * 4.0 + 2.5, 'duration': 0.4, 'velocity': 100}) # Nota de apoyo
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 3.5, 'duration': 0.25, 'velocity': 110}) # Cierre
|
||
|
||
elif style == 'dembow':
|
||
# Linea de bajo dembow caracteristica - sigue el patron del kick
|
||
for bar in range(bars):
|
||
# Nota raiz en tiempos fuertes con slide/portamento
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 0.0, 'duration': 0.5, 'velocity': 125}) # Beat 1 - fuerte
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 1.75, 'duration': 0.3, 'velocity': 100}) # Ghost con kick
|
||
notes.append({'pitch': root_midi, 'start': bar * 4.0 + 3.0, 'duration': 0.5, 'velocity': 120}) # Beat 3 - fuerte
|
||
# Slide a octava superior en el anticipo
|
||
notes.append({'pitch': root_midi + 12, 'start': bar * 4.0 + 3.75, 'duration': 0.2, 'velocity': 90}) # Slide up
|
||
|
||
else: # walking
|
||
for bar in range(bars):
|
||
for beat in range(4):
|
||
time = bar * 4.0 + beat
|
||
if beat == 0:
|
||
pitch = root_midi
|
||
elif beat == 1:
|
||
pitch = scale_notes[2] if len(scale_notes) > 2 else root_midi + 3
|
||
elif beat == 2:
|
||
pitch = scale_notes[3] if len(scale_notes) > 3 else root_midi + 5
|
||
else:
|
||
pitch = scale_notes[4] if len(scale_notes) > 4 else root_midi + 7
|
||
|
||
notes.append({'pitch': pitch, 'start': time, 'duration': 0.9, 'velocity': 100})
|
||
|
||
return notes
|
||
|
||
def create_chord_progression(self, key: str, progression_type: str, length: float) -> List[Dict]:
|
||
"""Crea una progresión de acordes"""
|
||
notes = []
|
||
|
||
# Parsear key
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
is_minor = 'm' in key.lower()
|
||
scale_name = 'minor' if is_minor else 'major'
|
||
|
||
root_midi = self.note_name_to_midi(root_note, 4) # Octava 4 para acordes
|
||
scale_notes = self.get_scale_notes(root_midi, scale_name)
|
||
|
||
# Seleccionar progresión
|
||
progressions = CHORD_PROGRESSIONS.get(progression_type, CHORD_PROGRESSIONS['techno'])
|
||
progression = random.choice(progressions)
|
||
|
||
bars = int(length / 4.0)
|
||
beats_per_bar = 4
|
||
|
||
for bar in range(bars):
|
||
degree = progression[bar % len(progression)] - 1
|
||
|
||
if degree < len(scale_notes):
|
||
chord_root = scale_notes[degree]
|
||
else:
|
||
chord_root = root_midi
|
||
|
||
# Construir acorde (triada)
|
||
third = 3 if 'minor' in scale_name else 4
|
||
chord_tones = [chord_root, chord_root + third, chord_root + 7]
|
||
|
||
# Stab chords - cortos y percusivos
|
||
if progression_type == 'techno':
|
||
for pitch in chord_tones:
|
||
notes.append({
|
||
'pitch': pitch,
|
||
'start': bar * beats_per_bar,
|
||
'duration': 0.25,
|
||
'velocity': 90
|
||
})
|
||
elif progression_type == 'house':
|
||
for beat in [0.5, 2.5]:
|
||
for pitch in chord_tones:
|
||
notes.append({
|
||
'pitch': pitch,
|
||
'start': bar * beats_per_bar + beat,
|
||
'duration': 0.5,
|
||
'velocity': 75
|
||
})
|
||
else:
|
||
# Default: acordes en beats 1 y 3
|
||
for beat in [0, 2]:
|
||
for pitch in chord_tones:
|
||
notes.append({
|
||
'pitch': pitch,
|
||
'start': bar * beats_per_bar + beat,
|
||
'duration': 1.0,
|
||
'velocity': 85
|
||
})
|
||
|
||
return notes
|
||
|
||
def create_melody(self, key: str, scale: str, length: float, genre: str) -> List[Dict]:
|
||
"""Crea una melodÃa/lead"""
|
||
notes = []
|
||
|
||
root_note = key[:-1] if len(key) > 1 else key
|
||
root_midi = self.note_name_to_midi(root_note, 5) # Octava 5 para lead
|
||
scale_notes = self.get_scale_notes(root_midi, scale)
|
||
|
||
bars = max(1, int(length / 4.0))
|
||
motif_pool = [
|
||
([0, 2, 4, 2, 5, 4], [0.0, 0.5, 1.5, 2.0, 2.75, 3.25]),
|
||
([0, 3, 4, 6, 4], [0.0, 0.75, 1.5, 2.5, 3.25]),
|
||
([0, 2, 3, 5, 3, 2], [0.0, 0.5, 1.0, 2.0, 2.5, 3.5]),
|
||
]
|
||
motif_steps, motif_times = random.choice(motif_pool)
|
||
|
||
for bar in range(bars):
|
||
bar_offset = bar * 4.0
|
||
phrase_shift = 0 if bar % 4 in [0, 1] else random.choice([0, 1, -1, 2])
|
||
invert_tail = (bar % 4 == 3)
|
||
for index, step in enumerate(motif_steps):
|
||
start = bar_offset + motif_times[index % len(motif_times)]
|
||
if start >= length:
|
||
continue
|
||
if invert_tail and index >= max(1, len(motif_steps) - 2):
|
||
start += 0.25
|
||
if random.random() < 0.18 and index not in [0, len(motif_steps) - 1]:
|
||
continue
|
||
|
||
scale_index = (step + phrase_shift) % len(scale_notes)
|
||
pitch = scale_notes[scale_index]
|
||
if genre in ['trance', 'progressive'] and index == len(motif_steps) - 1:
|
||
pitch += 12
|
||
elif genre in ['techno', 'tech-house'] and index % 3 == 2:
|
||
pitch -= 12
|
||
|
||
duration = 0.22 if start % 1.0 not in [0.0, 0.5] else 0.35
|
||
velocity = 78 + ((index + bar) % 3) * 8 + random.randint(-6, 8)
|
||
notes.append({
|
||
'pitch': pitch,
|
||
'start': start,
|
||
'duration': duration,
|
||
'velocity': max(60, min(123, velocity))
|
||
})
|
||
|
||
return notes
|