Bandas sonoras dinámicas y secuenciales para juegos

En este tutorial veremos una técnica para construir y secuenciar música dinámica para juegos. La construcción y la secuenciación ocurren en tiempo de ejecución, lo que permite a los desarrolladores de juegos modificar la estructura de la música para reflejar lo que está sucediendo en el mundo del juego..

Antes de saltar a los detalles técnicos, es posible que desee ver una demostración de esta técnica en acción. La música en la demostración se construye a partir de una colección de bloques de audio individuales que se secuencian y se mezclan en tiempo de ejecución para formar la pista musical completa.

Haga clic para ver la demostración..

Esta demostración requiere un navegador web que admita la API de audio web W3C y el audio OGG. Google Chrome es el mejor navegador para ver esta demostración, pero también se puede usar Firefox Aurora.

Si no puede ver la demostración anterior en su navegador, puede ver este video de YouTube en su lugar:



Visión general

La forma en que funciona esta técnica es bastante sencilla, pero tiene el potencial de agregar música muy buena y dinámica a los juegos si se utiliza de forma creativa. También permite crear pistas de música infinitamente largas a partir de un archivo de audio relativamente pequeño.

La música original está esencialmente deconstruida en una colección de bloques, cada uno de los cuales tiene una barra de longitud, y esos bloques se almacenan en un solo archivo de audio. El secuenciador de música carga el archivo de audio y extrae las muestras de audio sin procesar que necesita para reconstruir la música. La estructura de la música está dictada por una colección de arreglos mutables que le dicen al secuenciador cuándo tocar los bloques de música..

Puede pensar en esta técnica como una versión simplificada del software de secuenciación, como Reason, FL Studio o Dance EJay. También puedes pensar en esta técnica como el equivalente musical de los ladrillos Lego..


Estructura de archivos de audio

Como se mencionó anteriormente, el secuenciador de música requiere que la música original se descomponga en una colección de bloques, y esos bloques deben almacenarse en un archivo de audio..

Esta imagen muestra cómo se pueden almacenar los bloques en un archivo de audio..

En esa imagen puede ver que hay cinco bloques individuales almacenados en el archivo de audio, y todos los bloques tienen la misma longitud. Para mantener las cosas simples para este tutorial, los bloques tienen una barra de largo..

El orden de los bloques en el archivo de audio es importante porque dicta a qué secuencia de canales se asignan los bloques. El primer bloque (por ejemplo, batería) se asignará al primer canal del secuenciador, el segundo bloque (por ejemplo, la percusión) se asignará al segundo canal del secuenciador, y así sucesivamente.


Canales secuenciador

Un canal secuenciador representa una fila de bloques y contiene banderas (una para cada barra de música) que indican si se debe reproducir el bloque asignado al canal. Cada bandera es un valor numérico y es cero (no jugar el bloque) o uno (jugar el bloque).

Esta imagen muestra la relación entre los bloques y los canales del secuenciador..

Los números alineados horizontalmente a lo largo de la parte inferior de la imagen anterior representan números de barra. Como puedes ver, en el primer compás de música (01) solo se tocará el bloque de guitarra, pero en la quinta barra (05) Se tocarán los bloques de batería, percusión, bajo y guitarra..


Programación

En este tutorial no veremos el código de un secuenciador de música en pleno funcionamiento; en su lugar, veremos el código básico requerido para ejecutar un secuenciador de música simple. El código se presentará como pseudocódigo para que las cosas se mantengan lo más indiferentes al lenguaje posible.

Antes de comenzar, debe tener en cuenta que el lenguaje de programación que finalmente decida utilizar requerirá una API que le permita manipular el audio a un nivel bajo. Un buen ejemplo de esto es la Web Audio API disponible en JavaScript..

También puede descargar los archivos de origen adjuntos a este tutorial para estudiar una implementación de JavaScript de un secuenciador de música básico que se creó como una demostración de este tutorial..

Resumen rápido

Tenemos un solo archivo de audio que contiene bloques de música. Cada bloque de música tiene una barra de longitud, y el orden de los bloques en el archivo de audio determina el canal del secuenciador al que se asignan los bloques..

Constantes

Hay dos piezas de información que necesitaremos antes de poder continuar. Necesitamos conocer el ritmo de la música, en tiempos por minuto, y el número de tiempos en cada compás. Este último se puede considerar como la marca de tiempo de la música. Esta información debe almacenarse como valores constantes porque no cambia mientras se ejecuta el secuenciador de música.

 TEMPO = 100 // tiempos por minuto SIGNATURE = 4 // tiempos por compás

También necesitamos saber la frecuencia de muestreo que utiliza la API de audio. Por lo general, esto será de 44100 Hz, porque está perfectamente bien para el audio, pero algunas personas tienen su hardware configurado para usar una frecuencia de muestreo más alta. La API de audio que elija utilizar debe proporcionar esta información, pero para el propósito de este tutorial, asumiremos que la frecuencia de muestreo es de 44100 Hz..

 SAMPLE_RATE = 44100 // Hertz

Ahora podemos calcular la longitud de muestra de una barra de música, es decir, el número de muestras de audio en un bloque de música. Este valor es importante porque permite al secuenciador de música ubicar los bloques individuales de música y las muestras de audio dentro de cada bloque, en los datos del archivo de audio..

 BLOCK_SIZE = floor (SAMPLE_RATE * (60 / (TEMPO / SIGNATURE)))

Transmisiones de audio

La API de audio que elija utilizar determinará cómo se representarán las secuencias de audio (matrices de muestras de audio) en su código. Por ejemplo, la Web Audio API utiliza objetos AudioBuffer.

Para este tutorial habrá dos secuencias de audio. La primera transmisión de audio será de solo lectura y contendrá todas las muestras de audio cargadas desde el archivo de audio que contiene los bloques de música, esta es la transmisión de audio de "entrada".

El segundo flujo de audio será de solo escritura y se usará para enviar muestras de audio al hardware; Este es el flujo de audio de "salida". Cada una de estas corrientes se representará como una matriz unidimensional.

 entrada = […] salida = […]

El proceso exacto requerido para cargar el archivo de audio y extraer las muestras de audio del archivo será dictado por el lenguaje de programación que utilice. Con eso en mente, asumiremos la entrada La matriz de transmisión de audio ya contiene las muestras de audio extraídas del archivo de audio..

los salida Por lo general, la transmisión de audio tendrá una duración fija, ya que la mayoría de las API de audio le permitirán elegir la frecuencia con la que las muestras de audio deben procesarse y enviarse al hardware, es decir, con qué frecuencia actualizar Se invoca la función. La frecuencia normalmente está vinculada directamente a la latencia del audio, las frecuencias altas requerirán más potencia del procesador, pero darán como resultado latencias más bajas y viceversa..

Datos del secuenciador

Los datos del secuenciador son una matriz multidimensional; cada subgrupo representa un canal de secuenciador y contiene indicadores (uno para cada barra de música) que indican si el bloque de música asignado al canal debe reproducirse o no. La longitud de las matrices de canales también determina la duración de la música..

 canales = [[0,0,0,0, 0,0,0,0, 1,1,1,1, 1,1,1,1], // tambores [0,0,0,0, 1 , 1,1,1, 1,1,1,1, 1,1,1,1], // percusión [0,0,0,0, 0,0,0,0, 1,1,1, 1, 1,1,1,1], // bajo [1,1,1,1, 1,1,1,1, 1,1,1,1, 1,1,1,1], // guitarra [0,0,0,0, 0,0,1,1, 0,0,0,0, 0,0,1,1] // cuerdas]

Los datos que ves allí representan una estructura musical de dieciséis compases. Contiene cinco canales, uno para cada bloque de música en el archivo de audio, y los canales están en el mismo orden que los bloques de música en el archivo de audio. Las banderas en las matrices de canales nos permiten saber si el bloque asignado a los canales se debe reproducir o no: el valor 0 significa que no se jugará un bloque; el valor 1 significa que se jugará un bloque.

Esta estructura de datos es mutable, se puede cambiar en cualquier momento, incluso cuando el secuenciador de música se está ejecutando, y esto le permite modificar las banderas y la estructura de la música para reflejar lo que está sucediendo en un juego..

Procesamiento de audio

La mayoría de las API de audio transmitirán un evento a una función de controlador de eventos, o invocarán una función directamente, cuando necesite enviar más muestras de audio al hardware. Esta función se suele invocar constantemente como el bucle principal de actualización de un juego, pero no con tanta frecuencia, por lo que se debe dedicar tiempo a optimizarlo..

Básicamente lo que sucede en esta función es:

  1. Se extraen múltiples muestras de audio de la entrada transmisión de audio.
  2. Esas muestras se suman para formar una sola muestra de audio.
  3. Esa muestra de audio es empujada en el salida transmisión de audio.

Antes de llegar a las entrañas de la función, necesitamos definir un par de variables más en el código:

 playing = true // indica si la música (el secuenciador) está reproduciendo la posición = 0 // la posición de la cabeza lectora del secuenciador, en muestras

los jugando Boolean simplemente nos permite saber si la música se está reproduciendo; Si no se está reproduciendo, necesitamos introducir muestras de audio silenciosas en el salida transmisión de audio. los posición realiza un seguimiento de dónde está la cabeza lectora dentro de la música, por lo que es un poco como un limpiador en un reproductor de música o video típico.

Ahora para las entrañas de la función:

 función update () outputIndex = 0 outputCount = output.length if (playing == false) // las muestras silenciosas deben enviarse a la secuencia de salida while (outputIndex < outputCount )  output[ outputIndex++ ] = 0.0  // the remainder of the function should not be executed return  chnCount = channels.length // the length of the music, in samples musicLength = BLOCK_SIZE * channels[ 0 ].length while( outputIndex < outputCount )  chnIndex = 0 // the bar of music that the sequencer playhead is pointing at barIndex = floor( position / BLOCK_SIZE ) // set the output sample value to zero (silent) output[ outputIndex ] = 0.0 while( chnIndex < chnCount )  // check the channel flag to see if the block should be played if( channels[ chnIndex ][ barIndex ] == 1 )  // the position of the block in the "input" stream inputOffset = BLOCK_SIZE * chnIndex // index into the "input" stream inputIndex = inputOffset + ( position % BLOCK_SIZE ) // add the block sample to the output sample output[ outputIndex ] += input[ inputIndex ]  chnIndex++  // advance the playhead position position++ if( position >= musicLength) // restablecer la posición del cursor de reproducción para hacer un bucle en la posición de la música = 0 outputIndex ++

Como puede ver, el código requerido para procesar las muestras de audio es bastante simple, pero como este código se ejecutará varias veces por segundo, debe buscar formas de optimizar el código dentro de la función y pre-calcular tantos valores como sea posible. Las optimizaciones que puede aplicar al código dependen únicamente del lenguaje de programación que utilice.

No olvide que puede descargar los archivos de origen adjuntos a este tutorial si desea ver una forma de implementar un secuenciador de música básico en JavaScript mediante la API de audio web..


Notas

El formato del archivo de audio que utilice debe permitir que el audio se reproduzca sin problemas. En otras palabras, el codificador utilizado para generar el archivo de audio no debe inyectar ningún relleno (fragmentos de audio silenciosos) en el archivo de audio. Desafortunadamente, los archivos MP3 y MP4 no pueden usarse por esa razón. Se pueden usar archivos OGG (utilizados por la demostración de JavaScript). También puede usar archivos WAV si lo desea, pero no son una opción sensata para juegos o aplicaciones basados ​​en la web debido a su tamaño..

Si está programando un juego, y si el lenguaje de programación que está usando para el juego admite la concurrencia (subprocesos o trabajadores), es posible que desee considerar ejecutar el código de procesamiento de audio en su propio hilo o trabajador si es posible. Hacer eso aliviará el ciclo de actualización principal del juego de cualquier sobrecarga de procesamiento de audio que pueda ocurrir.


Música dinámica en juegos populares

La siguiente es una pequeña selección de juegos populares que aprovechan la música dinámica de una forma u otra. La implementación que estos juegos usan para su música dinámica puede variar, pero el resultado final es el mismo: los jugadores del juego tienen una experiencia de juego más inmersiva..

  • Viaje: thatgamecompany.com
  • Flor: thatgamecompany.com
  • LittleBigPlanet: littlebigplanet.com
  • Portal 2: thinkwithportals.com
  • PixelJunk Shooter: pixeljunk.jp
  • Red Dead Redemption: rockstargames.com
  • Uncharted: naughtydog.com

Conclusión

Entonces, aquí tienes, una implementación simple de música secuencial dinámica que puede mejorar realmente la naturaleza emotiva de un juego. La forma en que decida utilizar esta técnica y la complejidad del secuenciador depende de usted. Hay muchas instrucciones que puede tomar esta implementación simple y las cubriremos en un futuro tutorial..

Si tiene alguna pregunta, no dude en publicarlas en los comentarios a continuación y le responderé lo antes posible..