Programación de Audio Para Juego de Captura de Movimiento
¡Hola! Quería compartir algunas experiencias de mis prácticas de tres meses en Turku Game Lab en Finlandia. Tuve la oportunidad de trabajar en un proyecto que me pareció muy interesante, combinaba captura de movimiento con programación de audio. Como un apasionado de la programación y la música, esta fue una gran oportunidad para sumergirme en la programación de audio, algo que había querido probar desde hace tiempo.
¿De qué trata el proyecto?
El juego en el que estábamos trabajando era algo que podría salir de la serie WarioWare. En una escena ambientada en un circo, los jugadores actuaban como un payaso, haciendo poses al ritmo de la música. Usamos cámaras tipo Kinect para la captura de movimiento.
La idea básica era esta: Mientras sonaba la música, unos recortes con poses se movían hacia el avatar del jugador en pantalla. El jugador tenía que imitar la pose de la silueta que se acercaba y mantenerla durante aproximadamente medio segundo. Si lo lograba, la silueta se aceleraba y desaparecía, dando paso a la siguiente. La idea era establecer un flujo continuo y rítmico de movimiento.
¿Cómo acabé en este proyecto?
Inicialmente, el juego no contaba con elementos rítmicos. El equipo había implementado con éxito la captura de movimiento y la detección de colisiones para las siluetas. Durante una revisión del proyecto, nuestro tutor vino a probar el juego, discutir desafíos técnicos y hacer una lluvia de ideas.
En ese momento, había terminado un curso de Unity para familiarizarme con la programación de juegos y el Editor de Unity. Este curso fue clave para introducirme en conceptos comunes de desarrollo de juegos y me animó a experimentar añadiendo características a los minijuegos completados en cada sección.
Mientras los compañeros del laboratorio estaban con el tutor viendo cómo continuar con el proyecto, yo ya estaba experimentando con programación de audio y siguiendo algunos tutoriales que mencionaré más adelante. Cuando les escuché discutir la necesidad de añadir elementos rítmicos para mejorar el juego, me di cuenta de que podía contribuir. Vi una oportunidad perfecta para aplicar lo que sabía sobre audio y explorar más esta área antes de mi proyecto final de la FP. Me acerqué al equipo y les expliqué que había estado trabajando en algunos scripts que, con algunas mejoras, podrían proporcionar las mecánicas que estaban buscando.
Terminé asignado al proyecto y creé dos scripts principales:
Script de sincronización de ritmo.
El objetivo de este script era sincronizar eventos música. Dado un BPM (pulsos por minuto) y una fuente de audio, podía generar eventos que se sincronizaban con pulsos específicos. Así es como funcionaba:
El script tomaba el BPM de la canción como entrada.
- Calculamos el tiempo entre pulsos (60 segundos / BPM).
- Usando la propiedad
AudioSource.timeSamples
de Unity, referenciaba la posición actual de reproducción. - Dividiendo la posición actual de reproducción por las muestras(samples) por pulso, podía determinar cuándo ocurría un pulso.
- El script permitía diferentes subdivisiones rítmicas (negras, corcheas, etc.) usando multiplicadores.
- Los eventos podían activarse en pulsos o subdivisiones específicas, permitiendo una sincronización precisa de los elementos del juego.
En nuestro juego, usamos esto para empujar los recortes con siluetas en la escena de juego usando rítmicos específicos. Esto añadió ese elemento rítmico que el equipo estaba buscando, logrando un flujo de juego más inmersivo.
Mejoras en el script de sincronización de ritmo
Mientras trabajaba con el script, me di cuenta de que se necesitaban dos mejoras clave:
a) Delay inicial: Muchas grabaciones no comienzan precisamente en el primer pulso. Para tener esto en cuenta, implementé una característica de retraso de inicio usando una corrutina:
[SerializeField] private float _startDelay = 0f;
private bool _isReadyToTrackBeats = false;
private void Start()
{
StartCoroutine(StartAfterDelay(_startDelay));
}
private IEnumerator StartAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
_isReadyToTrackBeats = true;
}
Esto nos permite sincronizar con precisión los pulsos con el inicio real de los elementos rítmicos en la música, incluso si hay una introducción o entrada.
b) Desplazamiento de fase: El script original activaba los pulsos al final de cada división rítmica. Para proporcionar más flexibilidad y sincronización precisa, implementé un desplazamiento de fase:
[SerializeField] private float _phaseOffset = 0f;
public void CheckForNewPulse(float pulse)
{
int currentPulse = Mathf.FloorToInt(pulse + _phaseOffset);
if (currentPulse != _lastPulse)
{
_lastPulse = currentPulse;
_trigger.Invoke();
}
}
Este desplazamiento permite un control preciso sobre cuándo ocurren los disparos de beat. Por ejemplo, en un compás de 4/4, establecer el desplazamiento a 0.25 movería el disparo al tercer beat del compás, mientras que 0.5 lo movería al segundo beat. Esta característica abrió posibilidades creativas, como enfatizar los beats 2 y 4 en géneros de música popular.
Script de visualización de audio.
Este script analiza el flujo de audio en tiempo real y usa esos datos para impulsar elementos visuales. Aquí está el desglose:
- El script usaba el método
AudioSource.GetSpectrumData()
de Unity para obtener datos de frecuencia del audio. - Estos datos crudos se procesaban luego en bandas de frecuencia más manejables (usé 8 bandas en este caso).
- Cada banda representaba un rango de frecuencias, desde bajas (graves) hasta altas (agudos).
- Para hacer la visualización más cómoda se implementa un buffer.
- Estos datos procesados podían usarse luego para mover varios elementos visuales en la escena.
Como prueba configuré una escena con 8 cubos. La altura de cada cubo cambiaría en respuesta a su banda de frecuencia correspondiente - el cubo más a la izquierda reaccionaba a las frecuencias más bajas, mientras que el más a la derecha respondía a las frecuencias más altas.
Mejoras al script de visualización
Mientras trabajaba en este script, me di cuenta de que la forma en que las bandas de audio reaccionaban a diferentes frecuencias no coincidía con lo que estaba acostumbrado a ver en los ecualizadores que se utilizan en producción de música. Después de investigar, descubrí dos factores importantes a considerar: la naturaleza logarítmica de nuestra percepción del sonido y la tendencia de las frecuencias altas de tener amplitudes más bajas en señales de audio convencionales.
Para tener esto en cuenta, modifiqué el método MakeFrequencyBands()
:
for (int i = 0; i < 8; i++)
{
float average = 0;
int sampleCount = (int)Mathf.Pow(2, i) * 2;
if (i == 7)
sampleCount += 2;
for (int j = 0; j < sampleCount; j++)
{
// ... (channel selection code)
average += sample * (count + 1);
count++;
}
average /= count;
_freqBand[i] = average * 10;
}
Esta modificación aborda las particularidades mencionadas previamente:
- Distribución Logarítmica de las Frecuencias: Usando
Mathf.Pow(2, i) * 2
para calcular el número de muestras para cada banda de frecuencia resulta en bandas exponencialmente más anchas a medida que aumenta la frecuencia, coincidiendo con nuestra percepción del sonido según lo que indica la ley Weber-Fechner. - "Normalización" de la Amplitud: La línea
average += (_samplesLeft[count] + _samplesRight[count]) * (count + 1);
(así como sus variantes) aplican un factor de ponderación a cada muestra, aumentando de forma efectiva la amplitud de frecuencias más altas. De esta forma compensamos la tendencia natural de tener menos amplitud de las frecuencias más altas, de esta forma conseguimos una representación visual más equilibrada del espectro auditivo.
En el juego final, usamos este script para animar cabinas de altavoces en el fondo de la escena del circo. Como la música tiene bajos bien marcados, realmente se podía ver a los altavoces moviéndose con el ritmo, haciendo la escena más atractiva.
Desafíos y experiencias de aprendizaje
Mientras trabajaba en estos scripts, me encontré con algunos desafíos:
- Precisión en la detección de pulsos: Asegurar que la detección de pulsos fuera precisa y sincronizada fue complicado. Confiar en el hilo de actualización principal de Unity podría no dar una sincronización consistente si la sincronización debe mantenerse durante un periodo extendido de tiempo.
- Visualización de audio suave: Los sistemas de audio a menudo vienen con ecualizadores que se pueden ajustar, generalmente muestran algunas barras con diferentes frecuencias, si los datos de audio no se procesan antes de generar la visualización, esta se verá demasiado tosca para ser útil.
Al superar estos desafíos, mejoré mi comprensión de cómo se maneja el audio en entornos de juegos, la importancia de la optimización en aplicaciones en tiempo real y las complejidades de crear elementos de juego responsivos impulsados por música.
Conclusiones
El proceso de mejorar estos scripts me ayudó a solidificar lo que estaba aprendiendo, lo que podría haber terminado como scripts olvidados después de un proyecto se convirtió en cambio en un paso en mi esfuerzo por convertirme en un profesional con conocimientos de programación de audio o programación orientada a aplicaciones musicales.
Para hacer esta redacción breve, evité temas que surgieron durante las pruebas de los scripts y durante mi investigación, el enfoque de los scripts es bastante ingenuo, especialmente la sincronización de pulsos, en general el audio se gestiona en su propio hilo y esto también es cierto para Unity, para hacer una sincronización robusta que pueda mantenerse en el tiempo, una de las soluciones es crear un reloj capaz de mantener sincronizados el audio y la jugabilidad, sin embargo, esta no era una tarea factible de abordar para el desarrollo de este proyecto, pero si tienes curiosidad, hay un par de juegos de ritmo que aplican esta solución, no sólo son de código abierto, también son muy divertidos, estoy hablando de StepMania y el Osu-framework.
Referencias
- Puedes consultar la lista de reproducción de Peer's Play para la implementación básica del script de visualización.
- Para entender cómo generar pulsos, puedes consultar el video de b3agz.
- Puedes leer sobre la Ley de Weber-Fechner en Wikipedia, eso es lo que usé como referencia.
Por último, nada de esto habría sido posible sin mis colegas Mika, Mario y Lucas, trabajar con vosotros fue un placer y espero que se nos presente una oportunidad de trabajar juntos de nuevo. También gracias a nuestros tutores y seniors en Turku Game Lab, esta experiencia me ha ayudado a ampliar mis horizontes como programador.
PD: El repositorio está vacío actualmente, pero planeo subir el código pronto.