Música adaptativa con AI: capas dinámicas según gameplay
Generar variantes con MusicGen, mezclar capas (calmo / tensión / combate) en runtime, transiciones musicales que siguen al jugador. Pipeline indie sin contratar compositor.
Publicado: · Por Juanjo "Banyo" López
0. Introducción
Pones tu track de combate en loop. A los 90 segundos el jugador ya lo odia. A los 180 lo silencia. Y todavía no llegó al boss.
La música lineal aburre porque no responde a nada. La industria lo sabe desde hace 20 años: vertical remixing (sumar o restar capas instrumentales) y horizontal resequencing (encadenar segmentos según el estado del juego) son las dos técnicas canónicas para que la música siga al jugador en lugar de ignorarlo. Lo que cambió hace poco: ya no necesitas a un compositor cobrando por minuto para producir esas variantes. Modelos como MusicGen (Meta, 2023), Stable Audio y Suno generan tracks o stems a partir de un prompt en pocos segundos.
Esto no convierte a la AI en compositor de tu juego, pero sí en productor de variantes. Tú decides la dirección artística; el modelo te da las 30 piezas que necesitas para que la mezcla en runtime tenga material con qué jugar. Sirve para roguelikes donde la música escala con la horda (estilo Hades), platformers con tensión adaptativa, juegos narrativos con dos o tres moods por escena.
¿Qué cubre este tutorial?
- La diferencia entre vertical remixing y horizontal resequencing, y cuándo combinarlos.
- Cómo generar stems alineados (mismo BPM, mismo tono, misma duración) con MusicGen y compañía.
- El motor de mezcla en runtime: capas con volúmenes que sigues con
lerp, oAudioMixercon snapshots. - Cómo mapear el estado del juego a la mezcla sin que la música pegue saltos cada 0.1 segundos.
- Implementación en Unity con menos de 60 líneas de C#.
1. Demo
2. Concepto y arquitectura
La música adaptativa con AI combina dos partes: una pipeline de generación (MusicGen, Stable Audio u otros crean capas y variantes desde prompts) y un motor de mezcla en runtime (FMOD, Wwise o Unity AudioMixer cruza esas capas según el estado del juego). La AI hace lo que un compositor caro haría a mano; el motor de audio es exactamente el mismo de siempre.
Esa definición se desarma en varias piezas. Vamos una por una.
2.1 Vertical remixing vs horizontal resequencing
Son las dos técnicas base. No son rivales, se combinan.
- Vertical remixing: una sola pieza musical descompuesta en capas (drums, bass, lead, strings, pads). En runtime subes y bajas el volumen de cada una según el estado. La pieza no cambia, su densidad sí.
- Horizontal resequencing: varias secuencias (intro, verso, puente, clímax) que se encolan. Cuando termina una, el motor decide cuál sigue. Bueno para transiciones grandes: pasar de exploración a boss fight.
Cada fila es un stem; el tramo coloreado indica cuándo está audible según intensity. Drums siempre; bass entra pronto; lead solo en tensión; strings solo en peak.
Cada bloque es un segmento musical; las flechas son las transiciones que el motor decide en runtime. El loop interno de combat se mantiene mientras dura el estado.
Indie con presupuesto cero suele empezar por vertical: una pieza de 2 minutos con 4 stems da 16 combinaciones. Horizontal exige más material y un editor de transiciones, por eso suele venir después.
2.2 ¿Qué hace MusicGen y otros generadores de AI?
MusicGen (Meta, 2023) genera audio condicionado por texto: le pasas “epic orchestral, slow build, fantasy theme, 90 BPM, A minor” y te devuelve 30 segundos de audio. Stable Audio (Stability AI) hace lo mismo con foco en loops y stems. Suno y Udio apuntan a canciones completas con voz; menos útiles para gameplay, más para tracks finales.
Los puntos que te importan como dev:
- Stems separados: las versiones recientes de MusicGen y Stable Audio aceptan instrucciones tipo “drums only” o devuelven stems separados nativamente. Eso es lo que te permite mezcla vertical.
- Melody conditioning: puedes pasar una melodía de referencia para que las generaciones compartan estructura. Sin esto, cada generación es una pieza distinta.
- Determinismo por seed: misma seed + mismo prompt → mismo audio. Crítico para iterar.
No vas a generar audio en runtime. Generas offline, curas, importas como .wav o .ogg al proyecto. El runtime solo reproduce y mezcla.
2.3 ¿Cómo generar stems alineados (BPM, key, length)?
El problema número uno cuando empiezas: tus stems no calzan. El drum loop va a 92 BPM, el bass a 88, y la mezcla suena a borracho. Tres reglas para evitarlo:
- Mismo BPM en todos los prompts. “90 BPM, 4/4” va en cada prompt. No confíes en que el modelo lo respete; verifica el archivo con un detector de tempo (Audacity, librosa) y descarta los que se desvían más de 1 BPM.
- Misma tonalidad. “A minor” o “C major”. Si mezclas dos stems en tonalidades distintas, hay choque armónico audible incluso para oídos no entrenados.
- Misma duración exacta en muestras. Si el loop es de 8 compases a 90 BPM, dura 21.333 segundos. Recórtalo con muestra precisa en tu DAW. Un offset de 50 ms basta para que el loop se desfase tras 10 vueltas.
Cuando el modelo soporte melody conditioning o multi-stem generation, úsalo: una sola generación produce stems que ya están alineados por construcción.
2.4 Mezcla en runtime: el motor
Aquí la AI desaparece. El motor de audio es el de siempre.
Opción A — Unity AudioMixer con snapshots. Defines snapshots (Calm, Tension, Combat, Boss) cada uno con un set de volúmenes por grupo. En runtime llamas mixer.TransitionToSnapshots(...) con tiempo de crossfade. Simple, sin código por capa.
Opción B — Stems como AudioSource con volume lerpeado. Cada capa es un AudioSource con su clip. Cada frame ajustas source.volume hacia el target con Mathf.Lerp. Más control granular, más boilerplate. Lo que se ve en el snippet de Unity más abajo.
Opción C — FMOD o Wwise. Defines parameters (intensity, combatActive) y conectas sus rangos a volúmenes de tracks. El game code solo escribe el parameter; el middleware hace el mix. Es lo que usa la mayoría de la industria y la curva de aprendizaje vale el dolor a partir de cierto tamaño de proyecto.
La AI no participa en este paso. Genera material; el motor lo mezcla.
2.5 ¿Cómo se mapea el game state a la mezcla?
Calculas un escalar intensity ∈ [0, 1] desde el estado del juego y de ahí derivas el volumen de cada capa. Este es exactamente el mismo patrón del stress del Director AI — y por la misma razón: el jugador no debe sentir cambios bruscos.
intensity_raw = 0.5 * nearbyEnemiesNorm
+ 0.3 * (1 - hpRatio)
+ 0.2 * timeInCombatNorm
intensity = lerp(intensity, intensity_raw, 0.05)
Suaviza con un lerp con factor pequeño (0.05–0.1 por frame). Si el jugador entra en combate por 0.3 segundos y sale, no quieres que las strings hagan un crescendo y se corten. El suavizado convierte ruido en señal.
Después, mapea intensity a cada capa con curvas distintas:
- drums: siempre a volumen 1.0. Son la base, no entran ni salen.
- bass:
clamp01(intensity * 1.5). Entra fácil, está casi siempre presente. - lead:
smoothstep(0.3, 0.6, intensity). Aparece en tensión, no en calma. - strings:
smoothstep(0.6, 0.9, intensity). Solo en peak.
smoothstep da una curva en S que suena más musical que una rampa lineal. Las capas no se “encienden” — emergen.
2.6 Pre-generación: el pipeline real
Generas mucho y descartas mucho. Receta indie:
- Define 4–6 moods (
calm,mystery,tension,combat,victory,defeat). - Para cada mood, escribe 10–15 prompts variando instrumentos, tempo cercano y referencia estilística. Mantén BPM y key constantes dentro del mood.
- Genera 60–100 piezas, cada una 30 s a 3 minutos. Esto toma 1–2 horas con MusicGen en GPU local o algunos dólares en un endpoint hosted.
- Cura a mano. El 60% se descarta de entrada (tempo desviado, mix raro, glitches del modelo). De lo restante, selecciona las mejores 4–6 por mood.
- Recorta loop points en un DAW. Exporta como
.oggmono o estéreo según necesidad.
Final: 20–30 piezas usables, organizadas en carpetas por mood. Eso alimenta meses de gameplay.
2.7 ¿Streaming desde disco o todo en memoria?
Depende de la plataforma.
- PC / consola: cargar todo en memoria (
AudioClip.LoadType = DecompressOnLoad). Latencia cero al cambiar de capa. - Móvil: streaming desde disco (
LoadType = Streaming). Ahorra RAM, paga con un pequeño delay al hacer play. Solo dos o tresAudioSourcestreamean a la vez, así que define qué moods están “activos” y descarga los demás. - Web (WebGL): usa
CompressedInMemory. El navegador decodifica perezosamente.
Una hora de música a 128 kbps Vorbis pesa ~55 MB. Eso entra en cualquier plataforma actual, pero medirlo desde el día uno evita sorpresas.
2.8 Legal y ética
Antes de subir nada a Steam, revisa la licencia del modelo y del output.
- MusicGen (Meta) se publicó originalmente con licencia Creative Commons-NonCommercial para el modelo y MIT para el código. Versiones posteriores y forks ofrecen términos más laxos. Verifica la versión exacta que uses.
- Stable Audio: tiene tiers comerciales con licencia clara para uso en productos.
- Suno / Udio: licencia comercial atada a tu suscripción. Lee el TOS.
Tres prácticas que te ahorran problemas: (1) documenta qué modelo, qué versión, qué prompt y qué seed produjeron cada archivo; (2) edita el output (recorta, mezcla con tus propios efectos) — la “obra derivada” suele tener mejor estatus que el output crudo; (3) si tu juego va a una plataforma con políticas estrictas (algunas tiendas de consola), considera finetunear sobre un dataset propio o licenciar tracks tradicionales para las piezas signature. La AI cubre el material funcional, los humanos cubren el alma.
3. Pseudocódigo
class AdaptiveMusicSystem
layers: List<AudioSource> # drums, bass, lead, strings
targetWeights: Vector<Float>
currentWeights: Vector<Float>
intensity: Float = 0
smoothing: Float = 0.05
function update(system: AdaptiveMusicSystem, state: GameState, dt: Float)
raw = computeIntensity(state)
system.intensity = lerp(system.intensity, raw, system.smoothing)
system.targetWeights = mapIntensityToLayers(system.intensity)
for i in 0..system.layers.length
# crossfade suave en lugar de salto instantáneo
system.currentWeights[i] = lerp(
system.currentWeights[i],
system.targetWeights[i],
0.1
)
system.layers[i].volume = system.currentWeights[i]
function computeIntensity(state: GameState) -> Float
enemies = clamp01(state.nearbyEnemies / 5.0)
hpLack = 1.0 - state.hp / state.maxHp
combat = clamp01(state.timeInCombat / 10.0)
return clamp01(0.5 * enemies + 0.3 * hpLack + 0.2 * combat)
function mapIntensityToLayers(intensity: Float) -> Vector<Float>
return [
1.0, # drums: siempre
clamp01(intensity * 1.5), # bass: entra fácil
smoothstep(0.3, 0.6, intensity), # lead: tensión
smoothstep(0.6, 0.9, intensity), # strings: peak
]
El bucle es siempre el mismo: leer estado → calcular intensity → suavizar → mapear a pesos → mezclar. La AI no aparece. Su contribución vive en los AudioClip que cargaste, no en el código de mezcla.
4. Implementación en Unity / C#
using UnityEngine;
using UnityEngine.Audio;
public class AdaptiveMusic : MonoBehaviour {
[System.Serializable]
public class Layer {
public AudioSource source;
public AnimationCurve curve; // X: intensity 0..1, Y: volume 0..1
[HideInInspector] public float currentVolume;
}
[Header("Capas (drums, bass, lead, strings)")]
public Layer[] layers;
[Header("Suavizado")]
[Range(0f, 1f)] public float intensitySmoothing = 0.05f;
[Range(0f, 1f)] public float volumeSmoothing = 0.1f;
[Header("Estado del juego")]
public PlayerStats player;
public CombatTracker combat;
float intensity;
void Start() {
foreach (var l in layers) {
l.source.loop = true;
l.source.volume = 0f;
l.source.Play(); // todos arrancan a volumen 0
}
}
void Update() {
float raw = ComputeIntensity();
intensity = Mathf.Lerp(intensity, raw, intensitySmoothing);
foreach (var l in layers) {
float target = l.curve.Evaluate(intensity);
l.currentVolume = Mathf.Lerp(l.currentVolume, target, volumeSmoothing);
l.source.volume = l.currentVolume;
}
}
float ComputeIntensity() {
float enemies = Mathf.Clamp01(combat.NearbyEnemies / 5f);
float hpLack = 1f - player.HpRatio;
float inFight = Mathf.Clamp01(combat.TimeInCombat / 10f);
return Mathf.Clamp01(0.5f * enemies + 0.3f * hpLack + 0.2f * inFight);
}
}
Cada Layer tiene su AudioSource con su stem y una AnimationCurve editable en el inspector. Eso te da control artístico sin tocar código: ajustar cuándo entra cada capa es arrastrar puntos en una curva.
Si prefieres AudioMixer con snapshots, el patrón cambia: defines snapshots Calm, Tension, Combat en el asset del mixer, y desde código eliges entre snapshots con peso según intensity:
public AudioMixer mixer;
public AudioMixerSnapshot calm, tension, combat;
void ApplySnapshot(float intensity) {
var snapshots = new[] { calm, tension, combat };
var weights = new[] {
Mathf.Clamp01(1f - intensity * 2f),
Mathf.Clamp01(1f - Mathf.Abs(intensity - 0.5f) * 2f),
Mathf.Clamp01(intensity * 2f - 1f)
};
mixer.TransitionToSnapshots(snapshots, weights, 1.5f);
}
Esta variante es más declarativa: el mix vive en el asset, el código solo decide qué tan “en cada estado” estás.
5. En otros engines
- Godot: cada capa es un
AudioStreamPlayercon su propiobusen el AudioServer. Desde_process(delta)actualizasAudioServer.set_bus_volume_db(bus_idx, linear_to_db(volume)). La conversión a dB es lo único que cambia respecto a Unity. - Unreal: dos caminos. Sound Cues con
SoundCueparametrizados son el equivalente “low code” — defines mezclas y disparas conUGameplayStatics::PostEvent. MetaSounds + Audio Modulation te da nodos visuales con parámetros conectados a la lógica de gameplay, más cercano a FMOD/Wwise. - FMOD / Wwise: tu game code solo escribe parameters (
mixer.SetParameter("intensity", v)). El middleware tiene los gráficos de mezcla con las curvas y crossfades. Es lo que usa la mayoría de estudios medianos y grandes precisamente porque desacopla el rol del audio designer del rol del programador.
6. Quiz
Pon a prueba lo que entendiste
Responde una por una. La explicación aparece al elegir, correcta o no.
Generaste 4 stems con MusicGen y al mezclarlos suenan a borrachera: el drum se atrasa, el bass se desfasa. ¿Qué falta en tu pipeline de generación?
Tu música pega saltos bruscos cada vez que el jugador entra y sale de combate por medio segundo. La intensity está bien calculada. ¿Qué tocas?
Un colega te pregunta por qué no generas la música directamente en runtime con MusicGen en lugar de pre-generar y curar. ¿La razón principal?
Estás diseñando un juego narrativo con una banda sonora que es parte de la identidad de la marca (estilo Hollow Knight o Celeste). ¿Música adaptativa con AI es la solución?